blob: 521aa085f28ac0938b3c9acf7e70472e500d6b38 [file] [edit]
#include "builder/simplify.h"
#include <algorithm>
#include <cassert>
#include <cstdlib>
#include <deque>
#include <iostream>
#include <iterator>
#include <limits>
#include <optional>
#include <set>
#include <tuple>
#include <utility>
#include <vector>
#include "base/arithmetic.h"
#include "base/chrome_trace.h"
#include "builder/node_mutator.h"
#include "builder/rewrite.h"
#include "builder/substitute.h"
#include "runtime/depends_on.h"
#include "runtime/evaluate.h"
#include "runtime/expr.h"
#include "runtime/print.h"
namespace slinky {
namespace {
expr strip_boolean(expr x) {
if (const not_equal* ne = x.as<not_equal>()) {
if (is_zero(ne->b)) {
return strip_boolean(ne->a);
}
// This should be canonicalized to the RHS.
assert(!is_zero(ne->a));
}
return x;
}
bool deep_is_point(const interval_expr& x) { return x.is_point() || match(x.min, x.max); }
// Ensure that an interval that is a point in a deep equality sense is also a point in a shallow equality sense.
void ensure_is_point(interval_expr& x) {
if (!x.is_point() && match(x.min, x.max)) {
x.max = x.min;
}
}
// Rewrite `make_decl(block::make(stmts))` to be `block::make(make_decl(i) for i in stmts if i depends on sym else i)`.
template <class Fn>
stmt lift_decl_invariants(stmt body, var sym, Fn&& make_decl) {
if (const block* b = body.as<block>()) {
std::vector<stmt> result;
result.reserve(b->stmts.size());
for (auto i = b->stmts.begin(); i != b->stmts.end();) {
if (depends_on(*i, sym).any()) {
std::vector<stmt> result_i;
result_i.reserve(b->stmts.size());
do {
result_i.push_back(*i++);
} while (i != b->stmts.end() && depends_on(*i, sym).any());
if (result.empty() && i == b->stmts.end()) {
// Every stmt in the body depended on the decl, we aren't changing it.
return make_decl(std::move(body));
} else {
result.push_back(make_decl(block::make(std::move(result_i))));
}
if (i != b->stmts.end()) result.push_back(*i++);
} else {
result.push_back(*i++);
}
}
return block::make(std::move(result));
} else {
return make_decl(std::move(body));
}
}
// The algorithm at https://en.cppreference.com/w/cpp/algorithm/set_intersection, but detects any intersection.
template <typename It>
bool empty_intersection(It a_begin, It a_end, It b_begin, It b_end) {
It a = a_begin;
It b = b_begin;
while (a != a_end && b != b_end) {
if (*a == *b) {
return false;
} else if (*a < *b) {
++a;
} else {
++b;
}
}
return true;
}
template <typename T>
bool empty_intersection(const std::set<T>& a, const std::set<T>& b) {
return empty_intersection(a.begin(), a.end(), b.begin(), b.end());
}
// If a constant can be added to an expression without creating new nodes, this helper produces that expression,
// otherwise expr().
class constant_adder : public node_mutator {
public:
index_t c;
bool cancelled = false;
constant_adder(index_t c) : c(c) {}
void visit(const constant* op) override { set_result(op->value + c); };
void visit(const variable* op) override { set_result(expr()); }
void visit(const let* op) override {
expr body = mutate(op->body);
if (body.defined()) {
set_result(let::make(op->lets, std::move(body)));
} else {
set_result(expr());
}
}
template <typename T>
void visit_add_sub(const T* op, int sign_b) {
if (is_constant(op->b, c * -sign_b)) {
set_result(op->a);
cancelled = true;
return;
}
expr a = mutate(op->a);
if (a.defined()) {
set_result(T::make(std::move(a), op->b));
return;
}
c *= sign_b;
expr b = mutate(op->b);
c *= sign_b;
if (b.defined()) {
set_result(T::make(op->a, std::move(b)));
return;
}
set_result(expr());
}
void visit(const add* op) override { visit_add_sub(op, /*sign_b=*/1); }
void visit(const sub* op) override { visit_add_sub(op, /*sign_b=*/-1); }
template <typename T>
void visit_min_max(const T* op) {
// Here, we want to rewrite something like max(x + 2, y) - 2 to max(x, y - 2), but we don't want to rewrite
// something like max(x, 2) - 2 to max(x - 2, 0). To do this, we need to know if we cancelled something.
bool old_cancelled = cancelled;
cancelled = false;
expr a = mutate(op->a);
expr b = mutate(op->b);
cancelled = old_cancelled || cancelled;
if (a.defined() && b.defined()) {
set_result(T::make(std::move(a), std::move(b)));
} else if (a.defined() && cancelled) {
set_result(T::make(std::move(a), add::make(op->b, c)));
} else if (b.defined() && cancelled) {
set_result(T::make(add::make(op->a, c), std::move(b)));
} else {
set_result(expr());
}
}
void visit(const class min* op) override { visit_min_max(op); }
void visit(const class max* op) override { visit_min_max(op); }
void visit(const class select* op) override {
expr t = mutate(op->true_value);
if (t.defined()) {
expr f = mutate(op->false_value);
if (f.defined()) {
set_result(select::make(op->condition, std::move(t), std::move(f)));
return;
}
}
set_result(expr());
}
void visit(const mul* op) override {
if (auto b = as_constant(op->b)) {
// a*b + c == (a + c/b)*b if c%b == 0
if (*b != 0 && euclidean_mod(c, *b) == 0) {
c = euclidean_div(c, *b);
expr a = mutate(op->a);
c *= *b;
if (a.defined()) {
set_result(mul::make(std::move(a), op->b));
return;
}
}
}
set_result(expr());
}
void visit(const div* op) override { set_result(expr()); }
void visit(const mod* op) override { set_result(expr()); }
void visit(const equal*) override { set_result(expr()); }
void visit(const not_equal*) override { set_result(expr()); }
void visit(const less*) override { set_result(expr()); }
void visit(const less_equal*) override { set_result(expr()); }
void visit(const logical_and*) override { set_result(expr()); }
void visit(const logical_or*) override { set_result(expr()); }
void visit(const logical_not*) override { set_result(expr()); }
void visit(const call*) override { set_result(expr()); }
};
expr add_constant(const expr& a, index_t b) {
if (b == 0) return a;
return constant_adder(b).mutate(a);
}
// This is based on the simplifier in Halide: https://github.com/halide/Halide/blob/main/src/Simplify_Internal.h
class simplifier : public node_mutator {
public:
struct buffer_info {
expr elem_size;
// The dimension metadata for this buffer.
std::vector<dim_expr> dims;
// If true, we know that all the dimensions in the buffer are in the `dims` vector above. If not, there may be more
// dimensions we don't know about.
bool all_dims_known = false;
// The op that defined this buffer.
stmt decl;
// Identifies the buffer this buffer is a descendent of, if any.
var src;
// How many loops out is the `decl` found.
int loop_depth = 0;
buffer_info() = default;
buffer_info(expr elem_size) : elem_size(elem_size) {}
buffer_info(var sym, int rank) : dims(rank) {
elem_size = buffer_elem_size(sym);
for (int d = 0; d < rank; ++d) {
dims[d] = buffer_dim(sym, d);
}
}
void init_dims(var sym, int rank) {
for (int d = 0; d < static_cast<int>(dims.size()); ++d) {
if (!dims[d].bounds.min.defined()) dims[d].bounds.min = buffer_min(sym, d);
if (!dims[d].bounds.max.defined()) dims[d].bounds.max = buffer_max(sym, d);
if (!dims[d].stride.defined()) dims[d].stride = buffer_stride(sym, d);
if (!dims[d].fold_factor.defined()) dims[d].fold_factor = buffer_fold_factor(sym, d);
}
dims.reserve(rank);
for (int d = dims.size(); d < rank; ++d) {
dims.push_back(buffer_dim(sym, d));
}
}
};
struct expr_info {
interval_expr bounds;
alignment_type alignment;
expr replacement;
static expr_info substitution(expr replacement) {
expr_info result;
result.replacement = replacement;
return result;
}
var replacement_sym() const {
if (!replacement.defined()) return var();
auto result = as_variable(replacement);
assert(result);
return *result;
}
void trim_bounds_using_alignment() {
if (alignment.modulus == 0) {
bounds = point(alignment.remainder);
} else if (alignment.modulus > 1) {
auto bounds_min = as_constant(bounds.min);
if (bounds_min) {
index_t adjustment;
bool no_overflow =
!sub_with_overflow(alignment.remainder, euclidean_mod(*bounds_min, alignment.modulus), adjustment);
adjustment = euclidean_mod(adjustment, alignment.modulus);
index_t new_min;
no_overflow &= !add_with_overflow(*bounds_min, adjustment, new_min);
if (no_overflow) {
bounds.min = new_min;
}
}
auto bounds_max = as_constant(bounds.max);
if (bounds_max) {
index_t adjustment;
bool no_overflow =
!sub_with_overflow(euclidean_mod(*bounds_max, alignment.modulus), alignment.remainder, adjustment);
adjustment = euclidean_mod(adjustment, alignment.modulus);
index_t new_max;
no_overflow &= !sub_with_overflow(*bounds_max, adjustment, new_max);
if (no_overflow) {
bounds.max = new_max;
}
}
}
if (auto c = as_constant(bounds.as_point())) {
alignment.modulus = 0;
alignment.remainder = *c;
}
}
};
private:
symbol_map<buffer_info> buffers;
symbol_map<expr_info> vars;
bool proving = false;
expr_info result_info;
void set_result(expr e, expr_info info) {
assert(!result_info.bounds.min.defined() && !result_info.bounds.max.defined());
result_info = std::move(info);
ensure_is_point(result_info.bounds);
node_mutator::set_result(std::move(e));
}
void set_result(const base_expr_node* e, expr_info info) { set_result(expr(e), std::move(info)); }
void set_result(stmt s) { node_mutator::set_result(std::move(s)); }
void set_result(const base_stmt_node* s) { set_result(stmt(s)); }
// Dummy for template code.
void set_result(stmt s, const expr_info&) { set_result(std::move(s)); }
void set_result(const base_stmt_node* s, const expr_info&) { set_result(stmt(s)); }
public:
simplifier() {}
simplifier(const bounds_map& bounds, const alignment_map& alignment) {
for (size_t ix = 0; ix < bounds.size(); ix++) {
var id = var(ix);
if (!bounds[id]) continue;
vars[id] = {*bounds[id], alignment_type()};
}
for (size_t ix = 0; ix < alignment.size(); ix++) {
var id = var(ix);
if (!alignment[id]) continue;
if (vars[id]) {
vars[id]->alignment = *alignment[id];
} else {
vars[id] = {interval_expr(), *alignment[id]};
}
}
}
expr mutate(const expr& e, expr_info* info) {
expr result = node_mutator::mutate(e);
if (result.defined() && info) {
if (info != &result_info) {
*info = std::move(result_info);
}
} else {
result_info = {{expr(), expr()}, alignment_type()};
}
return result;
}
// Dummy for template code.
stmt mutate(const stmt& s, expr_info* info) { return node_mutator::mutate(s); }
expr mutate(const expr& e) override { return mutate(e, nullptr); }
stmt mutate(const stmt& s) override { return mutate(s, nullptr); }
// When mutating a value x interpreted as boolean, we need to effectively mutate x != 0, but we can't do that directly
// because it risks breaking the simplifiers ability to check if an expression has not changed. This helper emulates
// this.
expr mutate_boolean(const expr& e, expr_info* info) {
expr result = strip_boolean(mutate(e, info));
if (info && !is_boolean(result)) {
info->bounds = bounds_of(static_cast<const not_equal*>(nullptr), std::move(info->bounds), point(0));
}
return result;
}
void mutate_and_set_result(const expr& e) {
assert(!result_info.bounds.min.defined() && !result_info.bounds.max.defined());
node_mutator::set_result(mutate(e, &result_info));
}
interval_expr mutate(const interval_expr& x, expr_info* min_info, expr_info* max_info) {
if (deep_is_point(x)) {
expr result = mutate(x.min, min_info);
if (min_info && max_info) {
*max_info = *min_info;
}
return point(std::move(result));
} else {
interval_expr result = {mutate(x.min, min_info), mutate(x.max, max_info)};
// If the interval is of the form [select(b < a, b + 1, a), b], i.e. checking if the interval is empty, we can
// just rewrite it to [a, b], because all empty intervals are equivalent.
rewrite::pattern_wildcard<0> a;
rewrite::pattern_wildcard<1> b;
rewrite::match_context ctx;
if (match(ctx, select(b < a, b + 1, a), result.min) && match(ctx.matched(b), result.max)) {
result = {ctx.matched(a), ctx.matched(b)};
}
ensure_is_point(result);
return result;
}
}
interval_expr mutate(const interval_expr& x) override { return mutate(x, nullptr, nullptr); }
// This class manages information learned from conditions that can be used to improve bounds.
class knowledge {
// This could be made a variant if we need to be able to update more than one kind of symbol_map.
std::vector<scoped_value_in_symbol_map<expr_info>> vk;
std::vector<scoped_value_in_symbol_map<buffer_info>> bk;
std::vector<std::pair<expr, expr>> facts;
symbol_map<expr_info>& vars;
symbol_map<buffer_info>& buffers;
void add_var_info(const variable* x, interval_expr bounds, alignment_type alignment = {}) {
if (x->field == buffer_field::none) {
std::optional<expr_info> info = vars.lookup(x->sym);
if (!info) {
ensure_is_point(bounds);
info = {std::move(bounds), alignment};
} else {
info->bounds = simplify_intersection(std::move(info->bounds), std::move(bounds));
info->alignment = info->alignment & alignment;
}
if (auto value = as_constant(info->bounds.as_point())) {
// The bounds tell us this is a constant point.
info->replacement = *value;
}
vk.push_back(set_value_in_scope(vars, x->sym, std::move(info)));
} else {
expr value = bounds.as_point();
if (!value.defined() || !is_pure(value)) {
// TODO: Try to resolve the circular dependency that can result if we relax this constraint.
return;
}
var buf = x->sym;
if (std::find_if(bk.begin(), bk.end(), [buf](const auto& i) { return i.sym() == buf; }) == bk.end()) {
// Save the value if we haven't already, but we set the new values below.
bk.push_back(scoped_value_in_symbol_map<buffer_info>(buffers, buf));
}
std::optional<buffer_info>& info = buffers[buf];
if (x->dim >= 0) {
if (!info) {
info = buffer_info(buf, x->dim + 1);
} else {
info->init_dims(buf, x->dim + 1);
}
switch (x->field) {
case buffer_field::min: info->dims[x->dim].bounds.min = std::move(value); break;
case buffer_field::max: info->dims[x->dim].bounds.max = std::move(value); break;
case buffer_field::stride: info->dims[x->dim].stride = std::move(value); break;
case buffer_field::fold_factor: info->dims[x->dim].fold_factor = std::move(value); break;
default: break;
}
} else if (x->field == buffer_field::elem_size) {
if (!info) {
info = buffer_info(std::move(value));
} else {
info->elem_size = std::move(value);
}
} else if (x->field == buffer_field::rank) {
if (auto rank = as_constant(value)) {
if (!info) {
info = buffer_info(buf, *rank);
} else {
info->init_dims(buf, *rank);
}
info->all_dims_known = true;
}
}
}
}
public:
knowledge(symbol_map<expr_info>& vars, symbol_map<buffer_info>& buffers) : vars(vars), buffers(buffers) {}
knowledge(const knowledge&) = delete;
knowledge(knowledge&&) = default;
~knowledge() { exit_scope(); }
void exit_scope() {
// Destroy our knowledge in reverse order.
while (!vk.empty()) {
vk.pop_back();
}
while (!bk.empty()) {
bk.pop_back();
}
}
void learn_from_equal(const expr& a, const expr& b) {
if (const variable* v = a.as<variable>()) {
add_var_info(v, point(b));
} else if (const mod* md = a.as<mod>()) {
if (const variable* v = md->a.as<variable>()) {
if (auto m = as_constant(md->b)) {
if (auto r = as_constant(b)) {
add_var_info(v, {}, {*m, *r});
}
}
}
} else {
// If a is not a variable, then b is not a variable (because we canonicalize variables to the left hand side),
// so if we're going to learn from this, we need to just learn it as a fact.
facts.push_back({a == b, true});
}
if (const variable* v = b.as<variable>()) {
add_var_info(v, point(a));
}
}
void learn_from_less(const expr& a, const expr& b) {
if (const class max* r = b.as<class max>()) {
// a < max(x, y) ==> a < x && a < y
learn_from_less(a, r->a);
learn_from_less(a, r->b);
} else if (const class min* l = a.as<class min>()) {
// min(x, y) < b ==> x < b && y < b
learn_from_less(l->a, b);
learn_from_less(l->b, b);
} else {
const variable* av = a.as<variable>();
const variable* bv = b.as<variable>();
if (av) add_var_info(av, {expr(), simplify(static_cast<const add*>(nullptr), b, -1)});
if (bv) add_var_info(bv, {simplify(static_cast<const add*>(nullptr), a, 1), expr()});
if (!(av || bv)) {
// We couldn't learn from this, just remember it as a fact.
facts.push_back({a < b, true});
}
}
}
void learn_from_less_equal(const expr& a, const expr& b) {
if (const class max* r = b.as<class max>()) {
// a <= max(x, y) ==> a <= x && a <= y
learn_from_less_equal(a, r->a);
learn_from_less_equal(a, r->b);
} else if (const class min* l = a.as<class min>()) {
// min(x, y) <= b ==> x <= b && y <= b
learn_from_less_equal(l->a, b);
learn_from_less_equal(l->b, b);
} else {
const variable* av = a.as<variable>();
const variable* bv = b.as<variable>();
if (av) add_var_info(av, {expr(), b});
if (bv) add_var_info(bv, {a, expr()});
if (!(av || bv)) {
// We couldn't learn from this, just remember it as a fact.
facts.push_back({a <= b, true});
}
}
}
void learn_from_true(const expr& c) {
if (const logical_and* a = c.as<logical_and>()) {
learn_from_true(a->a);
learn_from_true(a->b);
} else if (const logical_not* n = c.as<logical_not>()) {
learn_from_false(n->a);
} else if (const less* lt = c.as<less>()) {
learn_from_less(lt->a, lt->b);
} else if (const less_equal* lt = c.as<less_equal>()) {
learn_from_less_equal(lt->a, lt->b);
} else if (const equal* eq = c.as<equal>()) {
learn_from_equal(eq->a, eq->b);
} else if (!as_constant(c) && !as_variable(c)) {
// We couldn't learn anything, just add the whole expression as a fact, if it isn't a constant or variable,
// which could rewrite the value from any x not zero to 1.
facts.push_back({c, expr(true)});
}
}
void learn_from_false(const expr& c) {
if (const logical_or* a = c.as<logical_or>()) {
learn_from_false(a->a);
learn_from_false(a->b);
} else if (const logical_not* n = c.as<logical_not>()) {
learn_from_true(n->a);
} else if (const less* lt = c.as<less>()) {
learn_from_less_equal(lt->b, lt->a);
} else if (const less_equal* lt = c.as<less_equal>()) {
learn_from_less(lt->b, lt->a);
} else if (const not_equal* ne = c.as<not_equal>()) {
learn_from_equal(ne->a, ne->b);
} else if (!as_constant(c)) {
// We couldn't learn anything, just add the whole expression as a fact.
facts.push_back({c, expr(false)});
}
}
expr substitute(expr x) const {
for (const auto& i : facts) {
x = slinky::substitute(x, i.first, i.second);
}
return x;
}
};
knowledge learn_from_true(const expr& c) {
knowledge result(vars, buffers);
result.learn_from_true(c);
return result;
}
knowledge learn_from_false(const expr& c) {
knowledge result(vars, buffers);
result.learn_from_false(c);
return result;
}
// When we attempt to prove things about bounds, we sometimes get constant expressions, but we can't recursively
// simplify without a high risk of infinite recursion. We can evaluate these as constants instead.
static bool prove_constant_true(const expr& e) {
if (!e.defined()) return false;
std::optional<index_t> ec = evaluate_constant(e);
if (ec) return *ec != 0;
// e is constant true if we know it has bounds that don't include zero.
std::optional<index_t> a = as_constant(constant_lower_bound(e));
if (a && *a > 0) return true;
std::optional<index_t> b = as_constant(constant_upper_bound(e));
return b && *b < 0;
}
static bool prove_constant_false(const expr& e) {
if (!e.defined()) return false;
std::optional<index_t> ec = evaluate_constant(e);
if (ec) return *ec == 0;
// e is constant false if we know its bounds are [0, 0].
std::optional<index_t> a = as_constant(constant_lower_bound(e));
if (!a) return false;
std::optional<index_t> b = as_constant(constant_upper_bound(e));
if (!b) return false;
return *a == 0 && *b == 0;
}
// Attempt to prove that the interval only contains true or false.
static std::optional<bool> attempt_to_prove(const interval_expr& e) {
if (prove_constant_true(e.min)) {
return true;
} else if (prove_constant_false(e.max)) {
return false;
} else {
return std::nullopt;
}
}
// Find the interval where `x` is true.
interval_expr where_true(const expr& x) {
scoped_trace trace("where_true");
expr_info info;
bool old_proving = proving;
proving = true;
mutate_boolean(x, &info);
proving = old_proving;
return info.bounds;
}
std::optional<bool> attempt_to_prove(const expr& e) { return attempt_to_prove(where_true(e)); }
bool prove_true(const expr& e) { return prove_constant_true(where_true(e).min); }
bool prove_false(const expr& e) { return prove_constant_false(where_true(e).max); }
void visit(const variable* op) override {
if (op->field != buffer_field::none) {
var new_sym = op->sym;
if (vars.contains(op->sym)) {
const expr_info& info = *vars[op->sym];
if (info.replacement.defined()) {
new_sym = info.replacement_sym();
}
}
const std::optional<buffer_info>& info = buffers[new_sym];
expr result = new_sym == op->sym ? expr(op) : variable::make(new_sym, op->field, op->dim);
expr bounds = result;
if (info) {
// TODO: We substitute here because we can't prove things like buffer_elem_size(x) == buffer_elem_size(y) where
// x is a crop of y. If we can fix that, we don't need to substitute here, which seems better.
auto visit_buffer_meta_value = [&, this](expr x) {
// There are many conditions in which we should substitute buffer meta:
// - We're being asked to substitute it (decl is undefined).
// - The value is something we should substitute (it's simple and pure).
// - The value is another buffer meta expression.
// - We're trying to prove something (as opposed to producing a simplified expression).
if (!info->decl.defined() || should_substitute(x) || x.as<variable>() || (proving && x.defined())) {
if (!match(x, op)) {
// This is a value we should substitute, and it's different from what we started with.
mutate_and_set_result(x);
return true;
}
}
if (x.defined()) {
bounds = x;
}
return false;
};
switch (op->field) {
case buffer_field::elem_size:
if (visit_buffer_meta_value(info->elem_size)) return;
break;
case buffer_field::min:
case buffer_field::max:
case buffer_field::stride:
case buffer_field::fold_factor:
if (op->dim < static_cast<index_t>(info->dims.size())) {
if (visit_buffer_meta_value(info->dims[op->dim].get_field(op->field))) return;
}
break;
default: break;
}
}
switch (op->field) {
case buffer_field::rank:
case buffer_field::elem_size: set_result(std::move(result), {{0, std::move(bounds)}, alignment_type()}); return;
case buffer_field::fold_factor: set_result(std::move(result), {{1, std::move(bounds)}, alignment_type()}); return;
default: set_result(std::move(result), {point(std::move(bounds)), alignment_type()}); return;
}
} else {
if (vars.contains(op->sym)) {
expr_info info = *vars[op->sym];
if (info.replacement.defined()) {
// TODO: This seems like it might be expensive, but it's the simplest way to get correct bounds and alignment
// information.
// TODO: Maybe we should intersect any information we already had with this?
mutate_and_set_result(info.replacement);
return;
} else if (auto c = as_constant(info.bounds.as_point())) {
set_result(info.bounds.min, {point(info.bounds.min), alignment_type()});
return;
} else {
if (!info.bounds.min.defined()) info.bounds.min = expr(op);
if (!info.bounds.max.defined()) info.bounds.max = expr(op);
set_result(op, std::move(info));
return;
}
}
}
set_result(op, {point(expr(op)), alignment_type()});
}
var visit_symbol(var x) {
if (vars.contains(x)) {
const expr_info& info = *vars[x];
if (info.replacement.defined()) {
return info.replacement_sym();
}
}
return x;
}
void visit(const constant* op) override { set_result(op, {point(expr(op)), {0, op->value}}); }
template <typename T>
void visit_min_max(const T* op) {
expr_info a_info;
expr a = mutate(op->a, &a_info);
expr_info b_info;
expr b = mutate(op->b, &b_info);
if (!a.defined() || !b.defined()) {
set_result(expr(), expr_info());
return;
}
// We need to check between the bounds and a/b themselves to avoid the possibility of something like:
// min(x, y + 1) not simplifying if we know the bounds of x are [0, y] and the bounds of y are [z, w],
// because we end up looking at min(y, z + 1) instead of min(y, y + 1).
// TODO: This is quite expensive, we should try to find a better way.
auto less_equal = [](const expr& a, const expr& a_max, const expr& b, const expr& b_min) {
return prove_constant_false(simplify(static_cast<const less*>(nullptr), b_min, a_max)) ||
(!match(a, a_max) && prove_constant_false(simplify(static_cast<const less*>(nullptr), b_min, a))) ||
(!match(b, b_min) && prove_constant_false(simplify(static_cast<const less*>(nullptr), b, a_max)));
};
if (less_equal(a, a_info.bounds.max, b, b_info.bounds.min)) {
if (T::static_type == expr_node_type::min) {
set_result(std::move(a), std::move(a_info));
} else {
set_result(std::move(b), std::move(b_info));
}
return;
} else if (less_equal(b, b_info.bounds.max, a, a_info.bounds.min)) {
if (T::static_type == expr_node_type::min) {
set_result(std::move(b), std::move(b_info));
} else {
set_result(std::move(a), std::move(a_info));
}
return;
}
expr result = simplify(op, std::move(a), std::move(b));
if (!result.same_as(op)) {
mutate_and_set_result(result);
} else {
set_result(std::move(result),
{bounds_of(op, std::move(a_info.bounds), std::move(b_info.bounds)), a_info.alignment | b_info.alignment});
}
}
void visit(const class min* op) override { visit_min_max(op); }
void visit(const class max* op) override { visit_min_max(op); }
alignment_type modulus_of(const add* op, const alignment_type& a, const alignment_type& b) { return a + b; }
alignment_type modulus_of(const sub* op, const alignment_type& a, const alignment_type& b) { return a - b; }
alignment_type modulus_of(const mul* op, const alignment_type& a, const alignment_type& b) { return a * b; }
alignment_type modulus_of(const div* op, const alignment_type& a, const alignment_type& b) { return a / b; }
alignment_type modulus_of(const mod* op, const alignment_type& a, const alignment_type& b) { return a % b; }
template <typename T>
void visit_binary(const T* op) {
expr_info a_info;
expr a = mutate(op->a, &a_info);
expr_info b_info;
expr b = mutate(op->b, &b_info);
if (!a.defined() || !b.defined()) {
set_result(expr(), expr_info());
return;
}
if (T::static_type == expr_node_type::mul) {
// TODO: This is really ugly, we should have a better way of expressing such simplifications.
if (auto c1 = as_constant(b)) {
if (const div* d = a.as<div>()) {
if (auto c0 = as_constant(d->b)) {
if (*c0 != 0) {
// This is (x/c0)*c1. If we know x is aligned to c0, the result is x*(c1/c0).
expr_info x_info;
expr x = mutate(d->a, &x_info);
if (x_info.alignment.modulus > 0 && x_info.alignment.modulus % *c0 == 0 &&
x_info.alignment.remainder % *c0 == 0) {
mutate_and_set_result((x * *c1) / *c0);
return;
}
}
}
}
}
}
auto a_mod = a_info.alignment.modulus;
auto a_rem = a_info.alignment.remainder;
rewrite::pattern_wildcard<0> x;
rewrite::pattern_constant<0> c0;
rewrite::pattern_constant<1> c1;
// It's really ugly to have rules here instead of simplify_rules, but plumbing bounds and alignment seems difficult.
if (T::static_type == expr_node_type::div) {
auto r = rewrite::make_rewriter(rewrite::pattern_expr{a} / rewrite::pattern_expr{b});
// Taken from https://github.com/halide/Halide/blob/main/src/Simplify_Div.cpp#L125-L167.
// clang-format off
if (r((x + c0) / c1, x / c1 + eval(a_rem / c1 - (a_rem - c0) / c1), eval(a_mod % c1 == 0)) ||
r((c0 - x) / c1, eval(a_rem / c1 + (c0 - a_rem) / c1) - x / c1, eval(a_mod % c1 == 0)) ||
false) {
mutate_and_set_result(r.result);
return;
}
// clang-format on
} else if (T::static_type == expr_node_type::mod) {
auto r = rewrite::make_rewriter(rewrite::pattern_expr{a} % rewrite::pattern_expr{b});
// Taken from https://github.com/halide/Halide/blob/main/src/Simplify_Div.cpp#L125-L167.
// clang-format off
if (r(x % c0, eval(a_rem % c0), eval(a_mod % c0 == 0)) ||
false) {
mutate_and_set_result(r.result);
return;
}
// clang-format on
}
expr result = simplify(op, std::move(a), std::move(b));
if (!result.same_as(op)) {
mutate_and_set_result(result);
} else {
expr_info info = {bounds_of(op, std::move(a_info.bounds), std::move(b_info.bounds)),
modulus_of(op, a_info.alignment, b_info.alignment)};
info.trim_bounds_using_alignment();
set_result(std::move(result), std::move(info));
}
}
void visit(const add* op) override {
if (auto bc = as_constant(op->b)) {
// We have a lot of rules that pull constants out of expressions. Sometimes we end up with complicated expressions
// that add a constant, e.g. select(x, max(2, y + 3), 4) - 1 and we could put that constant back inside. However,
// writing rules for all of these rewrites would be very tedious, so we handle it here instead.
expr result = add_constant(op->a, *bc);
if (result.defined()) {
mutate_and_set_result(result);
return;
}
}
visit_binary(op);
}
void visit(const sub* op) override { visit_binary(op); }
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); }
template <typename T>
void visit_logical(const T* op, bool coerce_boolean = false) {
expr_info a_info;
expr a = coerce_boolean ? mutate_boolean(op->a, &a_info) : mutate(op->a, &a_info);
expr_info b_info;
expr b = coerce_boolean ? mutate_boolean(op->b, &b_info) : mutate(op->b, &b_info);
if (!a.defined() || !b.defined()) {
set_result(expr(), expr_info());
return;
}
expr result = simplify(op, a, b);
if (!result.same_as(op)) {
mutate_and_set_result(result);
} else {
// Similar to the way we handle min/max, we need to check the bounds against the expression itself.
interval_expr result_bounds = bounds_of(op, a_info.bounds, b_info.bounds);
if (auto proven = attempt_to_prove(result_bounds)) {
set_result(expr(*proven), {point(*proven), alignment_type()});
} else if (auto proven = attempt_to_prove(bounds_of(op, point(a), b_info.bounds))) {
set_result(expr(*proven), {point(*proven), alignment_type()});
} else if (auto proven = attempt_to_prove(bounds_of(op, a_info.bounds, point(b)))) {
set_result(expr(*proven), {point(*proven), alignment_type()});
} else {
set_result(std::move(result), {std::move(result_bounds), alignment_type()});
}
}
}
void visit(const less* op) override { visit_logical(op); }
void visit(const less_equal* op) override { visit_logical(op); }
void visit(const equal* op) override { visit_logical(op); }
void visit(const not_equal* op) override { visit_logical(op); }
void visit(const logical_and* op) override { visit_logical(op, /*coerce_boolean=*/true); }
void visit(const logical_or* op) override { visit_logical(op, /*coerce_boolean=*/true); }
void visit(const logical_not* op) override {
expr_info info;
expr a = mutate_boolean(op->a, &info);
if (!a.defined()) {
set_result(expr(), expr_info());
} else if (auto proven = attempt_to_prove(info.bounds)) {
set_result(expr(!*proven), {point(!*proven), alignment_type()});
} else {
expr result = simplify(op, std::move(a));
if (result.same_as(op)) {
set_result(std::move(result), {bounds_of(op, std::move(info.bounds)), alignment_type()});
} else {
mutate_and_set_result(result);
}
}
}
void visit(const class select* op) override {
expr_info c_info;
// When simplifying expressions treated as bools, we need to force them to have the result 0 or 1.
expr c = mutate_boolean(op->condition, &c_info);
if (!c.defined()) {
set_result(expr(), expr_info());
return;
} else if (auto proven = attempt_to_prove(c_info.bounds)) {
mutate_and_set_result(*proven ? op->true_value : op->false_value);
return;
}
expr t, f;
expr_info t_info, f_info;
{
auto k = learn_from_true(c);
t = mutate(op->true_value, &t_info);
expr learned = k.substitute(t);
if (!learned.same_as(t)) {
t = mutate(learned, &t_info);
}
}
{
auto k = learn_from_false(c);
f = mutate(op->false_value, &f_info);
expr learned = k.substitute(f);
if (!learned.same_as(f)) {
f = mutate(learned, &f_info);
}
}
if (!t.defined() && !f.defined()) {
set_result(expr(), expr_info());
return;
}
expr e = simplify(op, std::move(c), std::move(t), std::move(f));
if (e.same_as(op)) {
expr_info info = {bounds_of(op, std::move(c_info.bounds), std::move(t_info.bounds), std::move(f_info.bounds)),
t_info.alignment | f_info.alignment};
info.trim_bounds_using_alignment();
set_result(std::move(e), std::move(info));
} else {
mutate_and_set_result(e);
}
}
static bool should_substitute(const expr& e) {
return e.as<constant>() || (e.as<variable>() && e.as<variable>()->field == buffer_field::none);
}
void visit(const call* op) override {
std::vector<expr> args;
std::vector<interval_expr> args_bounds;
args.reserve(op->args.size());
args_bounds.reserve(op->args.size());
bool changed = false;
for (const expr& i : op->args) {
expr_info i_info;
args.push_back(mutate(i, &i_info));
changed = changed || !args.back().same_as(i);
args_bounds.push_back(std::move(i_info.bounds));
}
if (op->intrinsic == intrinsic::buffer_at) {
assert(args.size() >= 1);
if (!args[0].defined()) {
set_result(expr(), expr_info());
return;
}
auto buf = as_variable(args[0]);
assert(buf);
const std::optional<buffer_info>& info = buffers[*buf];
if (info) {
for (int d = 0; d < static_cast<int>(std::min(info->dims.size(), args.size() - 1)); ++d) {
if (!info->dims[d].fold_factor.defined() && prove_true(args[d + 1] == info->dims[d].bounds.min)) {
// This argument is equal to the default value, and we know it is in bounds.
args[d + 1] = expr();
} else if (info->dims[d].fold_factor.defined() && prove_true(args[d + 1] == 0)) {
// This argument is equal to the default value, and we know it is in bounds.
args[d + 1] = expr();
}
}
}
} else if (op->intrinsic == intrinsic::abs) {
assert(args.size() == 1);
assert(args_bounds.size() == 1);
if (prove_constant_true(args_bounds[0].min >= 0)) {
set_result(std::move(args[0]), {std::move(args_bounds[0]), alignment_type()});
return;
} else if (prove_constant_true(args_bounds[0].max <= 0)) {
mutate_and_set_result(-args[0]);
return;
}
}
expr e = simplify(op, op->intrinsic, std::move(args));
if (e.same_as(op)) {
set_result(std::move(e), {bounds_of(op, std::move(args_bounds)), alignment_type()});
} else {
mutate_and_set_result(e);
}
}
template <typename T>
void visit_let(const T* op) {
std::vector<std::pair<var, expr>> lets;
lets.reserve(op->lets.size());
using sv_type = scoped_value_in_symbol_map<expr_info>;
std::vector<sv_type> scoped_values;
scoped_values.reserve(op->lets.size());
std::map<expr, var, node_less> reverse_lets;
bool values_changed = false;
for (const auto& s : op->lets) {
expr_info value_info;
expr value = mutate(s.second, &value_info);
var& name = reverse_lets[value];
if (name.defined()) {
// This value is the same as another let value, use that variable instead.
value = expr(name);
} else {
name = s.first;
}
if (should_substitute(value)) {
value_info = expr_info::substitution(std::move(value));
values_changed = true;
} else {
lets.emplace_back(s.first, std::move(value));
values_changed = values_changed || !lets.back().second.same_as(s.second);
}
assert(!vars.contains(s.first));
scoped_values.push_back(set_value_in_scope(vars, s.first, std::move(value_info)));
}
expr_info body_info;
auto body = mutate(op->body, &body_info);
scoped_values.clear();
for (auto it = lets.rbegin(); it != lets.rend();) {
auto deps = depends_on(body, it->first);
// Find any deps on this variable in the inner let values.
for (auto inner = lets.rbegin(); inner != it; ++inner) {
depends_on(inner->second, it->first, deps);
}
if (!deps.any()) {
// Prune dead lets
it = std::make_reverse_iterator(lets.erase(std::next(it).base()));
values_changed = true;
} else {
++it;
}
}
while (const T* let_body = body.template as<T>()) {
// Flatten nested lets
lets.insert(lets.end(), let_body->lets.begin(), let_body->lets.end());
body = let_body->body;
values_changed = true;
}
if (lets.empty()) {
// All lets were removed.
set_result(std::move(body), std::move(body_info));
} else if (!values_changed && body.same_as(op->body)) {
set_result(op, std::move(body_info));
} else {
set_result(T::make(std::move(lets), std::move(body)), std::move(body_info));
}
}
void visit(const let* op) override { visit_let(op); }
void visit(const let_stmt* op) override { visit_let(op); }
stmt mutate_with_buffer(const base_stmt_node* decl, stmt body, var buf, var src, std::optional<buffer_info> buffer) {
if (buffer) {
buffer->decl = stmt(decl);
buffer->src = src;
}
auto set_buffer = set_value_in_scope(buffers, buf, std::move(buffer));
assert(!vars.contains(buf));
return mutate(body);
}
// if `decl` is nullptr, the buffer will be substituted.
stmt mutate_with_buffer(const base_stmt_node* decl, stmt body, var buf, std::optional<buffer_info> buffer) {
if (buffer) buffer->decl = stmt(decl);
auto set_buffer = set_value_in_scope(buffers, buf, std::move(buffer));
return mutate(body);
}
stmt mutate_with_bounds(stmt body, var v, interval_expr bounds, alignment_type alignment = {}) {
assert(!vars.contains(v));
auto set_bounds = set_value_in_scope(vars, v, {std::move(bounds), alignment});
return mutate(body);
}
// Find all buffers accessed in `s`, adding them, and all the aliases of them, to `bufs`.
void buffers_accessed_via_aliases(const stmt& s, bool consumed, std::set<var>& bufs) {
std::vector<var> raw = find_buffer_dependencies(s, consumed, !consumed);
for (var i : raw) {
while (i.defined()) {
bufs.insert(i);
const std::optional<buffer_info>& info = buffers.lookup(i);
if (info && i != info->src) {
i = info->src;
} else {
break;
}
}
}
}
// Substitute expr() in for loop_var only in crop bounds within s, and only if those crop bounds do not crop a folded
// dimension.
stmt remove_loop_var_in_crop_bounds(const stmt& s, var loop_var) {
class m : public node_mutator {
symbol_map<buffer_info>& buffers;
var loop_var;
public:
m(symbol_map<buffer_info>& buffers, var loop_var) : buffers(buffers), loop_var(loop_var) {}
interval_expr mutate_crop_bounds(var src, int dim, const interval_expr& bounds) {
std::optional<buffer_info>& src_info = buffers[src];
if (!src_info || dim >= static_cast<int>(src_info->dims.size())) {
// We don't know about this buffer or dimension, it might be folded.
return bounds;
}
if (src_info->dims[dim].fold_factor.defined() && !is_constant(src_info->dims[dim].fold_factor, dim::unfolded)) {
// This dimension is folded, don't drop crops.
return bounds;
}
return substitute(bounds, loop_var, expr());
}
void visit(const crop_dim* op) override {
interval_expr bounds = mutate_crop_bounds(op->src, op->dim, op->bounds);
stmt body = mutate(op->body);
if (!bounds.same_as(op->bounds) || !body.same_as(op->body)) {
set_result(crop_dim::make(op->sym, op->src, op->dim, std::move(bounds), std::move(body)));
} else {
set_result(op);
}
}
void visit(const crop_buffer* op) override {
box_expr bounds(op->bounds.size());
bool changed = false;
for (std::size_t d = 0; d < op->bounds.size(); ++d) {
bounds[d] = mutate_crop_bounds(op->src, d, op->bounds[d]);
changed = changed || !bounds[d].same_as(op->bounds[d]);
}
stmt body = mutate(op->body);
if (changed || !body.same_as(op->body)) {
set_result(crop_buffer::make(op->sym, op->src, std::move(bounds), std::move(body)));
} else {
set_result(op);
}
}
};
return m(buffers, loop_var).mutate(s);
}
void visit(const loop* op) override {
interval_expr bounds = mutate(op->bounds);
expr step = mutate(op->step);
expr max_workers = mutate(op->max_workers);
// TODO: Try not to assume that step > 0.
auto knowledge = learn_from_true(step > 0);
if (prove_true(bounds.min > bounds.max)) {
// This loop is dead.
set_result(stmt());
return;
} else if (prove_true(bounds.min + step > bounds.max)) {
// The loop only runs at most once. It's safe to run the body even if the loop is empty, because we assume we can
// move loops freely in and out of calls, even if the buffers are empty.
auto s = set_value_in_scope(vars, op->sym, expr_info::substitution(bounds.min));
set_result(mutate(op->body));
return;
}
for (auto& i : buffers) {
if (i) ++i->loop_depth;
}
alignment_type alignment;
if (auto cstep = as_constant(step)) alignment.modulus = *cstep;
if (auto cmin = as_constant(bounds.min)) alignment.remainder = *cmin;
// If we're in the body of the loop, then we know that bounds.max >= bounds.min.
stmt body = mutate_with_bounds(op->body, op->sym, {bounds.min, mutate(max(bounds.min, bounds.max))}, alignment);
for (auto& i : buffers) {
if (i) --i->loop_depth;
}
scoped_trace trace("visit(const loop*)");
if (!body.defined()) {
set_result(stmt());
return;
} else if (!depends_on(body, op->sym).any()) {
// The body does not depend on the loop, drop the loop.
set_result(std::move(body));
return;
} else if (const block* b = body.as<block>()) {
scoped_trace trace("licm");
// This LICM is more aggressive than the typical compiler. Because we can freely add or remove loops around calls,
// if we find a loop invariant stmt that consumes a loop variant stmt, we can try to force the loop varying stmt
// to be loop invariant, and remove both from the loop.
// These accumulate results in reverse order.
// Here, we keep the original stmt, in case we need to put it back in the loop.
std::vector<std::pair<stmt, stmt>> lifted;
std::vector<stmt> loop_body;
loop_body.reserve(b->stmts.size());
// Find out if i produces something consumed by a loop invariant.
for (auto ri = b->stmts.rbegin(); ri != b->stmts.rend(); ++ri) {
stmt i = *ri;
if (!depends_on(i, op->sym).var) {
// i is loop invariant. Add it to the lifted result.
lifted.push_back({i, i});
} else {
// i depends on the loop. We are effectively reordering result ahead of i, we need to make sure we can do
// that.
std::set<var> produced_by_i;
buffers_accessed_via_aliases(i, /*consumed=*/false, produced_by_i);
// The loop invariants might depend on i. If so, we need to figure out what to do.
// We need to start with the first statement we lifted (lifted is in reverse order, so we insert this before
// the first dependency.
for (std::size_t j = lifted.size(); j > 0 && j <= lifted.size();) {
std::set<var> consumed_by_j;
buffers_accessed_via_aliases(lifted[j - 1].first, /*consumed=*/true, consumed_by_j);
if (empty_intersection(produced_by_i, consumed_by_j)) {
// This loop invariant is independent of i.
--j;
continue;
}
// j depends on i, so j is not loop invariant. Can we make i loop invariant? We can if we can delete the
// references to the loop variable.
stmt i_lifted = remove_loop_var_in_crop_bounds(i, op->sym);
if (!depends_on(i_lifted, op->sym).var) {
// We made i loop invariant, add it to the loop invariant result before it is used.
lifted.insert(lifted.begin() + j, {mutate(i_lifted), i});
--j;
i = stmt();
} else {
// We can't delete the references to the loop variable here, so j is not loop invariant. Put the original
// stmts back in the loop.
// TODO: We could be a bit more careful here, and only put the stmts that actually depend on this stmt
// back in the loop.
std::transform(lifted.begin(), lifted.begin() + j, std::back_inserter(loop_body),
[](const auto& i) { return i.second; });
lifted.erase(lifted.begin(), lifted.begin() + j);
}
break;
}
if (i.defined()) {
// We didn't lift i, put it in the loop.
loop_body.push_back(std::move(i));
}
}
}
if (!lifted.empty()) {
// We found something to lift out of the loop.
std::vector<stmt> result;
result.reserve(lifted.size() + 1);
for (auto i = lifted.rbegin(); i != lifted.rend(); ++i) {
result.push_back(i->first);
}
std::reverse(loop_body.begin(), loop_body.end());
result.push_back(
mutate(loop::make(op->sym, std::move(max_workers), bounds, step, block::make(std::move(loop_body)))));
set_result(block::make(std::move(result)));
return;
} else {
// We didn't find anything to lift out of the loop, proceed on to other possible simplifications.
}
}
if (prove_true(max_workers == loop::serial)) {
scoped_trace trace("drop_loop");
// Due to either scheduling or other simplifications, we can end up with a loop that runs a single call or copy on
// contiguous crops of a buffer. In these cases, we can drop the loop in favor of just calling the body on the
// union of the bounds covered by the loop.
stmt result = body;
std::vector<std::tuple<var, var, int, interval_expr>> new_crops;
bool drop_loop = true;
while (true) {
// For now, we only handle crop_dim. I don't think crop_buffer can ever yield this simplification?
if (const crop_dim* crop = result.as<crop_dim>()) {
// If we can prove that the union of either the current and next iteration, or previous and current iteration,
// is the whole iteration domain, then we can drop the loop. It's helpful to check both, because usually
// clamps that make this proof hard only exist in one direction.
interval_expr next_iter = substitute(crop->bounds, op->sym, expr(op->sym) + op->step);
interval_expr prev_iter = substitute(crop->bounds, op->sym, expr(op->sym) - op->step);
auto set_bounds_of_sym = set_value_in_scope(vars, op->sym, {bounds, alignment_type()});
// TODO: Currently we only support crops that monotonically increase the crop bounds as the loop progresses.
if (prove_true((next_iter.min > crop->bounds.min && crop->bounds.max + 1 >= next_iter.min) ||
(crop->bounds.min > prev_iter.min && prev_iter.max + 1 >= crop->bounds.min))) {
result = crop->body;
expr_info info_of_min, info_of_max;
mutate(crop->bounds, &info_of_min, &info_of_max);
interval_expr crop_bounds = info_of_min.bounds | info_of_max.bounds;
// If the original loop was empty, we need to hack the crop bounds to produce an empty buffer.
crop_bounds.min = select(bounds.max < bounds.min, crop_bounds.max + 1, crop_bounds.min);
new_crops.emplace_back(crop->sym, crop->src, crop->dim, std::move(crop_bounds));
} else {
// This crop was not contiguous, we can't drop the loop.
drop_loop = false;
break;
}
} else if (result.as<call_stmt>() || result.as<copy_stmt>()) {
// We've found the actual body of the loop.
break;
} else {
// TODO: We might be able to handle other cases too, like blocks of copies all to the same buffer (a
// concatenate?).
drop_loop = false;
break;
}
}
if (drop_loop) {
// Rewrite the crops to cover the whole loop, and drop the loop.
for (auto i = new_crops.rbegin(); i != new_crops.rend(); ++i) {
result = crop_dim::make(std::get<0>(*i), std::get<1>(*i), std::get<2>(*i), std::get<3>(*i), result);
}
set_result(mutate(result));
return;
}
}
if (bounds.same_as(op->bounds) && step.same_as(op->step) && max_workers.same_as(op->max_workers) &&
body.same_as(op->body)) {
set_result(op);
} else {
set_result(loop::make(op->sym, std::move(max_workers), std::move(bounds), std::move(step), std::move(body)));
}
}
void visit(const call_stmt* op) override {
call_stmt::symbol_list inputs = op->inputs;
call_stmt::symbol_list outputs = op->outputs;
bool changed = false;
auto visit_symbol_list = [&, this](call_stmt::symbol_list& list) {
for (var& i : list) {
var new_i = visit_symbol(i);
if (new_i != i) {
i = new_i;
changed = true;
}
}
};
visit_symbol_list(inputs);
visit_symbol_list(outputs);
if (changed) {
set_result(call_stmt::make(op->target, std::move(inputs), std::move(outputs), op->attrs));
} else {
set_result(op);
}
}
void visit(const copy_stmt* op) override {
var src = visit_symbol(op->src);
var dst = visit_symbol(op->dst);
var pad = visit_symbol(op->pad);
std::vector<scoped_value_in_symbol_map<expr_info>> decls;
for (var i : op->dst_x) {
decls.push_back(set_value_in_scope(vars, i, expr_info()));
}
std::vector<expr> src_x;
src_x.reserve(op->src_x.size());
bool changed = false;
for (const expr& i : op->src_x) {
src_x.push_back(mutate(i));
changed = changed || !src_x.back().same_as(i);
}
if (changed || src != op->src || dst != op->dst || pad != op->pad) {
set_result(copy_stmt::make(src, std::move(src_x), dst, op->dst_x, pad));
} else {
set_result(op);
}
}
void visit(const block* op) override {
std::vector<stmt> stmts;
stmts.reserve(op->stmts.size());
bool changed = false;
// Learn from checks in this scope and store the knowledge in k.
knowledge k(vars, buffers);
for (const stmt& s : op->stmts) {
stmts.push_back(mutate(s));
if (const check* c = stmts.back().as<check>()) {
k.learn_from_true(c->condition);
}
changed = changed || !stmts.back().same_as(s);
}
if (!changed) {
set_result(op);
} else {
set_result(block::make(std::move(stmts)));
}
}
dim_expr mutate(const dim_expr& d) {
dim_expr result = {mutate(d.bounds), mutate(d.stride), mutate(d.fold_factor)};
if (is_constant(result.fold_factor, dim::unfolded)) result.fold_factor = expr();
if (result.fold_factor.defined() &&
prove_true(result.min() / result.fold_factor == result.max() / result.fold_factor))
result.fold_factor = expr();
if (is_constant(result.stride, dim::auto_stride)) result.stride = expr();
return result;
}
template <typename T>
bool mutate_buffer(const T* op, buffer_info& info) {
scoped_trace trace("mutate_buffer");
info = buffer_info(mutate(op->elem_size));
bool changed = !info.elem_size.same_as(op->elem_size);
info.dims.reserve(op->dims.size());
for (std::size_t d = 0; d < op->dims.size(); ++d) {
info.dims.push_back(mutate(op->dims[d]));
changed = changed || !info.dims.back().same_as(op->dims[d]);
}
info.all_dims_known = true;
info.decl = stmt(op);
return changed;
}
void visit(const allocate* op) override {
buffer_info info;
bool changed = mutate_buffer(op, info);
stmt body = mutate_with_buffer(op, op->body, op->sym, info);
scoped_trace trace("visit(const allocate*)");
auto deps = depends_on(body, op->sym);
if (!deps.any()) {
set_result(std::move(body));
return;
} else if (!deps.buffer_data()) {
// We only needed the buffer meta, not the allocation itself.
set_result(mutate_with_buffer(nullptr, op->body, op->sym, std::move(info)));
return;
}
stmt before, after;
if (const block* b = body.as<block>()) {
// Split the body into 3 parts: the part that depends on the allocation, and anything before or after that.
const auto depends_on_alloc = [=](const stmt& s) { return depends_on(s, op->sym).any(); };
auto end_before = std::find_if(b->stmts.begin(), b->stmts.end(), depends_on_alloc);
if (end_before != b->stmts.end()) {
before = block::make({b->stmts.begin(), end_before});
auto end_body = std::find_if(b->stmts.rbegin(), b->stmts.rend(), depends_on_alloc).base();
after = block::make({end_body, b->stmts.end()});
body = block::make({end_before, end_body});
} else {
set_result(block::make({b->stmts.begin(), end_before}));
return;
}
}
if (changed || !body.same_as(op->body)) {
set_result(block::make({std::move(before),
allocate::make(op->sym, op->storage, std::move(info.elem_size), std::move(info.dims), std::move(body)),
std::move(after)}));
} else {
set_result(op);
}
}
// If d is equal to buffer_dim(sym, x), return x, otherwise return -1.
int is_buffer_dim(const dim_expr& d, var sym) {
if (is_variable(d.bounds.min, sym, buffer_field::min)) {
int dim = d.bounds.min.as<variable>()->dim;
if (is_variable(d.bounds.max, sym, buffer_field::max, dim) &&
is_variable(d.stride, sym, buffer_field::stride, dim) &&
is_variable(d.fold_factor, sym, buffer_field::fold_factor, dim)) {
return dim;
}
}
return -1;
}
// Returns true if x is equivalent to a variable (sym, field, dim).
bool is_buffer_meta(
const expr& x, const expr& value, var sym, buffer_field field, int dim, const expr& def = expr()) {
if (!x.defined() && !value.defined()) return true;
if (is_variable(x, sym, field, dim)) return true;
if (prove_true(x == value)) return true;
if (!x.defined() && def.defined() && prove_true(value == def)) return true;
return false;
}
// Returns true if d can be represented as buffer_dim(sym, dim)
bool is_buffer_dim(const dim_expr& d, const dim_expr& src, var sym, int dim) {
if (!is_buffer_meta(d.bounds.min, src.bounds.min, sym, buffer_field::min, dim)) return false;
if (!is_buffer_meta(d.bounds.max, src.bounds.max, sym, buffer_field::max, dim)) return false;
if (prove_true(src.bounds.min == src.bounds.max)) {
// The extent is 1, the stride and fold factor don't matter.
return true;
} else {
return is_buffer_meta(d.stride, src.stride, sym, buffer_field::stride, dim, slinky::dim::auto_stride) &&
is_buffer_meta(d.fold_factor, src.fold_factor, sym, buffer_field::fold_factor, dim, slinky::dim::unfolded);
}
}
// If we know that buffer metadata has some values, rewrite references to that dim to use buffer intrinsics
// when those references use the same values.
void canonicalize_buffer_meta(expr& x, const expr& value, buffer_field field, var sym) {
if (!is_variable(x, sym, field) && prove_true(x == value)) x = variable::make(sym, field);
}
void canonicalize_buffer(buffer_info& buf, const buffer_info& src, var sym) {
scoped_trace trace("canonicalize_buffer");
canonicalize_buffer_meta(buf.elem_size, src.elem_size, buffer_field::elem_size, sym);
for (int buf_d = 0; buf_d < static_cast<int>(buf.dims.size()); ++buf_d) {
dim_expr& d = buf.dims[buf_d];
// Try buf_d first, to prefer making identical buffers.
if (buf_d < static_cast<int>(src.dims.size()) && is_buffer_dim(d, src.dims[buf_d], sym, buf_d)) {
d = buffer_dim(sym, buf_d);
} else {
for (int src_d = 0; src_d < static_cast<int>(src.dims.size()); ++src_d) {
if (src_d != buf_d && is_buffer_dim(d, src.dims[src_d], sym, src_d)) {
d = buffer_dim(sym, src_d);
break;
}
}
}
}
}
void visit(const make_buffer* op) override {
expr base = mutate(op->base);
buffer_info info;
bool changed = mutate_buffer(op, info);
// To avoid redundant nested simplifications, try to substitute the buffer both before and after mutating the body.
// TODO: It may be impossible for depends_on_result::buffer_data() to change due to simplification, so the second
// check below could be unnecessary.
if (can_substitute_buffer(depends_on(op->body, op->sym))) {
// We only needed the buffer meta, not the buffer itself.
set_result(mutate_with_buffer(nullptr, op->body, op->sym, find_buffer_data_dependency(base), std::move(info)));
return;
}
if (const call* bc = as_intrinsic(base, intrinsic::buffer_at)) {
// Check if this make_buffer is equivalent to transpose, slice_buffer or crop_buffer
auto src_buf = as_variable(bc->args[0]);
assert(src_buf);
const std::optional<buffer_info>& src_info = buffers[*src_buf];
if (src_info) {
if (bc->args.size() == 1) {
if (const make_buffer* src = src_info->decl.as<make_buffer>()) {
// The src of this make_buffer is also a make_buffer, try to substitute the outer one.
base = src->base;
info.elem_size = substitute_buffer(info.elem_size, src->sym, src->elem_size, src->dims);
for (dim_expr& i : info.dims) {
i.bounds = substitute_buffer(i.bounds, src->sym, src->elem_size, src->dims);
i.stride = substitute_buffer(i.stride, src->sym, src->elem_size, src->dims);
i.fold_factor = substitute_buffer(i.fold_factor, src->sym, src->elem_size, src->dims);
}
set_result(mutate(make_buffer::make(op->sym, base, info.elem_size, info.dims, op->body)));
return;
}
} else {
// We don't know how to substitute non-trivial buffer_at calls yet.
}
// Before trying to match a more specialized op, try to normalize the dimensions to be in terms of src_buf
// metadata.
canonicalize_buffer(info, *src_info, *src_buf);
}
auto make_truncate = [&](var src, std::size_t rank, stmt body) {
if (src_info && src_info->all_dims_known && src_info->dims.size() == rank) {
// We know all the dims, and the rank is already what we want to truncate to.
return body;
}
// In this special case, we allow shadowing.
return transpose::make_truncate(src, src, rank, std::move(body));
};
if (match(info.elem_size, buffer_elem_size(*src_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.
auto is_slice = [&]() {
int dim = 0;
std::size_t slice_rank = 0;
std::size_t at_rank =
std::count_if(bc->args.begin() + 1, bc->args.end(), [](const expr& i) { return i.defined(); });
for (int d = 0; d < static_cast<int>(info.dims.size() + at_rank); ++d) {
if (d + 1 < static_cast<index_t>(bc->args.size()) && bc->args[d + 1].defined()) {
// Skip this dimension.
++dim;
} else if (slice_rank < info.dims.size()) {
// This arg is undefined. We need to find the next dimension here to be a slice.
if (is_buffer_dim(info.dims[slice_rank++], *src_buf) != dim++) return false;
} else {
return false;
}
}
return slice_rank == info.dims.size();
};
if (is_slice()) {
// make_buffer drops trailing dims, do the same here.
stmt body = make_truncate(op->sym, info.dims.size(), op->body);
std::vector<expr> at(bc->args.begin() + 1, bc->args.end());
set_result(mutate(slice_buffer::make(op->sym, *src_buf, 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.
auto is_crop = [&]() {
if (bc->args.size() > info.dims.size() + 1) return false;
for (index_t d = 0; d < static_cast<index_t>(info.dims.size()); ++d) {
if (!is_variable(info.dims[d].stride, *src_buf, buffer_field::stride, d) ||
!is_variable(info.dims[d].fold_factor, *src_buf, buffer_field::fold_factor, d)) {
return false;
}
// If the argument to buffer_at 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(*src_buf, d);
if (!match(info.dims[d].bounds.min, crop_min)) {
return false;
}
}
return true;
};
if (is_crop()) {
// make_buffer drops trailing dims, do the same here.
stmt body = make_truncate(op->sym, info.dims.size(), op->body);
set_result(mutate(crop_buffer::make(op->sym, *src_buf, dims_bounds(info.dims), std::move(body))));
return;
}
// To be a transpose, we need buffer_at to be the base of src_buf, and each dimension to be a dimension of the
// original buffer.
// TODO: This could probably be built into the slice check above.
std::vector<int> permutation;
auto is_transpose = [&]() {
if (bc->args.size() != 1) return false;
permutation.reserve(info.dims.size());
for (std::size_t d = 0; d < info.dims.size(); ++d) {
int dim = is_buffer_dim(info.dims[d], *src_buf);
if (dim >= 0) {
permutation.push_back(dim);
} else {
return false;
}
}
return true;
};
if (is_transpose()) {
set_result(mutate(transpose::make(op->sym, *src_buf, std::move(permutation), op->body)));
return;
}
}
}
stmt body = mutate_with_buffer(op, op->body, op->sym, find_buffer_data_dependency(base), info);
scoped_trace trace("visit(const make_buffer*)");
changed = changed || !base.same_as(op->base);
auto make_make_buffer = [&](stmt body) {
auto deps = depends_on(body, op->sym);
if (!deps.any()) {
// This make_buffer is unused.
return body;
} else if (can_substitute_buffer(deps)) {
// We only needed the buffer meta, not the buffer itself.
return mutate_with_buffer(nullptr, body, op->sym, find_buffer_data_dependency(base), info);
} else if (changed || !body.same_as(op->body)) {
return make_buffer::make(op->sym, base, info.elem_size, info.dims, std::move(body));
} else {
return stmt(op);
}
};
set_result(lift_decl_invariants(body, op->sym, make_make_buffer));
}
void visit(const constant_buffer* op) override {
buffer_info info(op->value->elem_size);
info.dims.resize(op->value->rank);
for (std::size_t d = 0; d < op->value->rank; ++d) {
info.dims[d] = dim_expr(op->value->dim(d));
}
info.all_dims_known = true;
info.decl = stmt(op);
stmt body = mutate_with_buffer(op, op->body, op->sym, info);
auto make_constant_buffer = [&](stmt body) {
auto deps = depends_on(body, op->sym);
if (!deps.any()) {
// This constant_buffer is unused.
return body;
} else if (can_substitute_buffer(deps)) {
// We only needed the buffer meta, not the buffer itself.
return mutate_with_buffer(nullptr, body, op->sym, info);
} else if (!body.same_as(op->body)) {
return constant_buffer::make(op->sym, op->value, std::move(body));
} else {
return stmt(op);
}
};
set_result(lift_decl_invariants(body, op->sym, make_constant_buffer));
}
std::optional<buffer_info> get_buffer_info(var buf, int rank) {
std::optional<buffer_info> info = buffers[buf];
if (!info) {
info = buffer_info(buf, rank);
} else {
info->init_dims(buf, rank);
}
return info;
}
template <typename T>
static void enumerate_bounds(expr x, std::set<expr, node_less>& bounds, index_t offset = 0) {
if (const add* a = x.as<add>()) {
if (auto c = as_constant(a->b)) {
// Remove constant adds and remember the offset for later.
x = a->a;
offset += *c;
}
}
auto add_offset = [offset](expr x) {
return offset ? simplify(static_cast<const class add*>(nullptr), std::move(x), offset) : x;
};
if (const T* t = x.as<T>()) {
enumerate_bounds<T>(t->a, bounds, offset);
enumerate_bounds<T>(t->b, bounds, offset);
} else if (const class select* s = x.as<class select>()) {
// Move constants into select here.
if (offset) {
bounds.insert(select(s->condition, add_offset(s->true_value), add_offset(s->false_value)));
} else {
bounds.insert(x);
}
} else {
bounds.insert(add_offset(std::move(x)));
}
}
template <typename T>
expr remove_redundant_bounds(expr x, const std::set<expr, node_less>& bounds, const expr& def) {
auto is_redundant = [&](const expr& x) {
// A bound x is redundant if the bounds already have a value i such that T(x, i) == i
for (const expr& i : bounds) {
if (std::is_same<T, class min>::value && prove_true(i <= x)) return true;
if (std::is_same<T, class max>::value && prove_true(i >= x)) return true;
}
return false;
};
if (is_redundant(x)) return def;
if (const T* t = x.as<T>()) {
bool a_redundant = is_redundant(t->a);
bool b_redundant = is_redundant(t->b);
if (a_redundant && b_redundant) {
return def;
} else if (a_redundant) {
return remove_redundant_bounds<T>(t->b, bounds, def);
} else if (b_redundant) {
return remove_redundant_bounds<T>(t->a, bounds, def);
}
} else if (const add* xa = x.as<add>()) {
if (as_constant(xa->b)) {
// We have T(x + y, b). We can rewrite to T(x, b - y) + y, and if we can eliminate the bound, the whole
// bound is redundant.
for (const expr& i : bounds) {
expr removed = remove_redundant_bounds<T>(xa->a, {mutate(i - xa->b)}, def - xa->b);
if (!removed.same_as(xa->a)) {
return std::move(removed) + xa->b;
}
}
}
} else if (const div* xa = x.as<div>()) {
if (as_constant(xa->b)) {
// We have T(x / y, b). We can rewrite to T(x, b * y) / y, and if we can eliminate the bound, the whole
// bound is redundant.
for (const expr& i : bounds) {
expr removed = remove_redundant_bounds<T>(xa->a, {mutate(i * xa->b)}, def * xa->b);
if (!removed.same_as(xa->a)) {
return std::move(removed) / xa->b;
}
}
}
} else if (const mul* xa = x.as<mul>()) {
if (as_constant(xa->b)) {
// We have T(x * y, b). We can do something similar to the above, but we need to be careful because division
// is not invertible. Since we're looking for bounds, we can use a conservative rounding. If we're looking for
// redundant mins, we should round up, and redundant maxes should round down.
for (const expr& i : bounds) {
auto div = [](const expr& a, const expr& b) {
return std::is_same<T, class min>::value ? ceil_div(a, b) : floor_div(a, b);
};
expr removed = remove_redundant_bounds<T>(xa->a, {mutate(div(i, xa->b))}, div(def, xa->b));
if (!removed.same_as(xa->a)) {
return std::move(removed) * xa->b;
}
}
}
} else if (const class select* xs = x.as<class select>()) {
for (const expr& i : bounds) {
if (const class select* bs = i.as<class select>()) {
if (match(xs->condition, bs->condition)) {
// We have T(select(c, xt, xf), select(c, bt, bf)), rewrite to select(c, T(xt, bt), T(xf, bf)) and attempt
// to eliminate bounds.
expr t = remove_redundant_bounds<T>(xs->true_value, {bs->true_value}, def);
expr f = remove_redundant_bounds<T>(xs->false_value, {bs->false_value}, def);
if (!t.same_as(xs->true_value) || !f.same_as(xs->false_value)) {
return select(xs->condition, std::move(t), std::move(f));
}
}
}
}
// Also try select(x, T(xt, b), T(xf, b))
expr t = remove_redundant_bounds<T>(xs->true_value, bounds, def);
expr f = remove_redundant_bounds<T>(xs->false_value, bounds, def);
if (!t.same_as(xs->true_value) || !f.same_as(xs->false_value)) {
return select(xs->condition, std::move(t), std::move(f));
}
}
return x;
}
interval_expr mutate_crop_bounds(const interval_expr& crop, var buf, int dim, interval_expr& buffer) {
if (!crop.min.defined() && !crop.max.defined()) return crop;
scoped_trace trace("mutate_crop_bounds");
interval_expr result = mutate(crop);
// Find and remove redundant clamps in the crop bounds.
// TODO: This seems like a hack. We should be able to do this with the simplifier itself. We basically want to do
// something like:
//
// result = simplify(result & x, {{x, buffer}})
//
// and then remove the clamps of x. But this is pretty tricky.
std::set<expr, node_less> mins = {buffer_min(buf, dim)};
std::set<expr, node_less> maxs = {buffer_max(buf, dim)};
enumerate_bounds<class max>(buffer.min, mins);
enumerate_bounds<class min>(buffer.max, maxs);
interval_expr deduped = {
remove_redundant_bounds<class max>(result.min, mins, buffer_min(buf, dim)),
remove_redundant_bounds<class min>(result.max, maxs, buffer_max(buf, dim)),
};
if (!deduped.same_as(result)) {
result = mutate(deduped);
}
// We might have written a select into an interval that tries to preserve the empty-ness of the interval.
// But this might be unnecessary. Try to remove unnecessary selects here.
rewrite::pattern_wildcard<0> x;
rewrite::pattern_wildcard<1> y;
rewrite::pattern_wildcard<2> z;
rewrite::pattern_wildcard<3> w;
rewrite::match_context ctx;
if (match(ctx, select(x < y, z, w), result.min)) {
if (is_variable(ctx.matched(x), buf, buffer_field::max, dim) &&
is_variable(ctx.matched(y), buf, buffer_field::min, dim)) {
// This select is a check that the dimension we are cropping is empty.
// If the buffer is empty, it doesn't matter what we do, the resulting crop will still be empty, so we can
// just take the new min.
result.min = ctx.matched(w);
}
} else if (match(ctx, x + min(y, z), result.min)) {
// This is the same as above, but the select was simplified.
if (is_variable(ctx.matched(y), buf, buffer_field::max, dim) || match(ctx.matched(y), result.max)) {
result.min = mutate(ctx.matched(x) + ctx.matched(z));
} else if (is_variable(ctx.matched(z), buf, buffer_field::max, dim) || match(ctx.matched(z), result.max)) {
result.min = mutate(ctx.matched(x) + ctx.matched(y));
}
}
if (result.min.defined() && prove_true(result.min <= buffer.min)) result.min = expr();
if (result.max.defined() && prove_true(result.max >= buffer.max)) result.max = expr();
// TODO: I think it might be possible to avoid generating these min/max + mutate in some cases.
if (result.min.defined()) buffer.min = mutate(max(buffer.min, result.min));
if (result.max.defined()) buffer.max = mutate(min(buffer.max, result.max));
return result;
}
static bool crop_needed(const depends_on_result& deps) {
// We don't need a crop if the buffer is only used as an input to a call. But we do need the crop if it is used as
// an input to a copy, which uses the bounds of the input for padding.
return deps.buffer_output || deps.buffer_src || deps.buffer_dst || deps.buffer_bounds;
}
void visit_crop(const base_stmt_node* op, var op_sym, var op_src, box_expr op_bounds, stmt op_body) {
std::optional<buffer_info> info = get_buffer_info(op_src, op_bounds.size());
while (info && info->decl.defined() && info->loop_depth == 0) {
if (const crop_buffer* c = info->decl.as<crop_buffer>()) {
// Substitute the outer crop bounds into this crop's bounds.
auto c_dims = make_dims_from_bounds(c->bounds);
for (interval_expr& i : op_bounds) {
i = substitute_buffer(i, op_src, c_dims);
}
// Nested crops of the same buffer, and the crop isn't used.
op_bounds.resize(std::max(op_bounds.size(), c->bounds.size()));
op_bounds = c->bounds & op_bounds;
op_src = c->src;
info = get_buffer_info(op_src, op_bounds.size());
op = nullptr;
} else if (const crop_dim* c = info->decl.as<crop_dim>()) {
// Substitute the outer crop bounds into this crop's bounds.
auto c_dims = make_dims_from_bounds(c->dim, c->bounds);
for (interval_expr& i : op_bounds) {
i = substitute_buffer(i, op_src, c_dims);
}
// Nested crops of the same buffer, and the crop isn't used.
op_bounds.resize(std::max<int>(op_bounds.size(), c->dim + 1));
op_bounds[c->dim] = c->bounds & op_bounds[c->dim];
op_src = c->src;
info = get_buffer_info(op_src, op_bounds.size());
op = nullptr;
} else {
break;
}
}
// If possible, rewrite crop_buffer of one dimension to crop_dim.
bool changed = op == nullptr;
box_expr bounds(op_bounds.size());
for (index_t i = 0; i < static_cast<index_t>(op_bounds.size()); ++i) {
bounds[i] = mutate_crop_bounds(op_bounds[i], op_src, i, info->dims[i].bounds);
changed = changed || !bounds[i].same_as(op_bounds[i]);
}
// Remove trailing undefined bounds.
while (!bounds.empty() && !bounds.back().min.defined() && !bounds.back().max.defined()) {
bounds.pop_back();
}
if (bounds.empty()) {
// This crop was a no-op.
if (op_src != op_sym) {
auto s = set_value_in_scope(vars, op_sym, expr_info::substitution(variable::make(op_src)));
set_result(mutate(op_body));
} else {
set_result(mutate(op_body));
}
return;
}
stmt body = mutate_with_buffer(op, op_body, op_sym, op_src, std::move(info));
scoped_trace trace("visit_crop");
auto deps = depends_on(body, op_sym);
if (!deps.any()) {
set_result(std::move(body));
return;
} else if (!crop_needed(deps)) {
set_result(substitute(body, op_sym, op_src));
return;
}
// If this was a crop_buffer, and we only have one dim, we're going to change it to a crop_dim.
const int dims_count = std::count_if(
bounds.begin(), bounds.end(), [](const interval_expr& i) { return i.min.defined() || i.max.defined(); });
changed = changed || (dims_count == 1 && op->type != crop_dim::static_type) || !body.same_as(op_body);
auto make_crop = [&](const stmt& body) -> stmt {
if (const crop_dim* c = body.as<crop_dim>()) {
if (dims_count == 1 && static_cast<int>(bounds.size()) == c->dim + 1 && c->src == op_src &&
match(c->bounds, bounds.back())) {
// This is an identical crop, re-use the buffer.
return crop_dim::make(op_sym, c->src, c->dim, c->bounds, substitute(c->body, c->sym, op_sym));
}
} else if (const crop_buffer* c = body.as<crop_buffer>()) {
if (c->src == op_src && match(c->bounds, bounds)) {
// This is an identical crop, re-use the buffer.
return crop_buffer::make(op_sym, c->src, c->bounds, substitute(c->body, c->sym, op_sym));
}
}
if (!changed && body.same_as(op_body)) {
return stmt(op);
} 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>(bounds.size()) - 1;
return crop_dim::make(op_sym, op_src, d, bounds[d], body);
} else {
return crop_buffer::make(op_sym, op_src, bounds, body);
}
};
set_result(lift_decl_invariants(body, op_sym, make_crop));
}
void visit(const crop_buffer* op) override {
var src = visit_symbol(op->src);
visit_crop(src == op->src ? op : nullptr, op->sym, src, op->bounds, op->body);
}
void visit(const crop_dim* op) override {
var src = visit_symbol(op->src);
box_expr bounds(op->dim + 1);
bounds[op->dim] = op->bounds;
visit_crop(src == op->src ? op : nullptr, op->sym, src, std::move(bounds), op->body);
}
void visit_slice(const base_stmt_node* op, var op_sym, var op_src, const std::vector<expr>& op_at, stmt op_body) {
std::vector<expr> at(op_at.size());
bool changed = op == nullptr;
std::optional<buffer_info> info = buffers[op_src];
for (index_t i = static_cast<index_t>(op_at.size()) - 1; i >= 0; --i) {
if (op_at[i].defined()) {
at[i] = mutate(op_at[i]);
changed = changed || !at[i].same_as(op_at[i]);
if (info && static_cast<index_t>(info->dims.size()) > i) info->dims.erase(info->dims.begin() + i);
}
}
stmt body = mutate_with_buffer(op, op_body, op_sym, op_src, std::move(info));
if (!depends_on(body, op_sym).any()) {
set_result(std::move(body));
return;
}
// Remove trailing undefined bounds.
while (!at.empty() && !at.back().defined()) {
at.pop_back();
}
changed = changed || at.size() != op_at.size() || !body.same_as(op_body);
// If this was a slice_buffer, and we only have one dimension, we're going to change it to a slice_dim.
const int at_count = std::count_if(at.begin(), at.end(), [](const expr& i) { return i.defined(); });
changed = changed || (at_count == 1 && op->type != slice_dim::static_type);
auto make_slice = [&](const stmt& body) -> stmt {
if (!changed && body.same_as(op)) {
return stmt(op);
} else if (at_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;
return slice_dim::make(op_sym, op_src, d, at[d], body);
} else {
return slice_buffer::make(op_sym, op_src, at, body);
}
};
if (at.empty()) {
// This slice was a no-op.
set_result(substitute(body, op_sym, op_src));
} else {
set_result(lift_decl_invariants(body, op_sym, make_slice));
}
}
void visit(const slice_buffer* op) override {
var src = visit_symbol(op->src);
visit_slice(src == op->src ? op : nullptr, op->sym, src, op->at, op->body);
}
void visit(const slice_dim* op) override {
var src = visit_symbol(op->src);
std::vector<expr> at(op->dim + 1);
at[op->dim] = op->at;
visit_slice(src == op->src ? op : nullptr, op->sym, src, at, op->body);
}
void visit(const transpose* op) override {
const std::optional<buffer_info>* src_info = &buffers[op->src];
var src = visit_symbol(op->src);
std::vector<int> dims = op->dims;
while (src_info && *src_info) {
if (const transpose* t = (*src_info)->decl.as<transpose>()) {
if (t->sym == src) {
// This is a transpose of another transpose. Rewrite this to directly transpose the parent.
dims = permute(dims, t->dims);
src = t->src;
src_info = &buffers[src];
continue;
}
}
break;
}
buffer_info sym_info{expr()};
if (src_info && *src_info) {
if (transpose::is_truncate(dims) && (*src_info)->all_dims_known && (*src_info)->dims.size() <= dims.size()) {
// transpose can't add dimensions.
assert((*src_info)->dims.size() == dims.size());
// This truncate is a no-op.
auto s = set_value_in_scope(vars, op->sym, expr_info::substitution(variable::make(src)));
set_result(mutate(op->body));
return;
}
sym_info.elem_size = (*src_info)->elem_size;
// This is like `permute`, but we can't guarantee that we know all the dimensions of src_info (it could be a
// buffer external to the pipeline).
sym_info.dims.resize(op->dims.size());
sym_info.all_dims_known = true;
for (size_t i = 0; i < op->dims.size(); ++i) {
if (op->dims[i] < static_cast<int>((*src_info)->dims.size())) {
sym_info.dims[i] = (*src_info)->dims[op->dims[i]];
} else {
sym_info.dims[i] = buffer_dim(op->src, op->dims[i]);
}
}
}
stmt body = mutate_with_buffer(op, op->body, op->sym, src, std::move(sym_info));
auto make_transpose = [&](stmt body) {
auto deps = depends_on(body, op->sym);
if (!deps.any()) {
return body;
} else if (!deps.buffer_dims) {
return substitute(body, op->sym, src);
} else if (body.same_as(op->body) && src == op->src && dims == op->dims) {
return stmt(op);
} else {
return transpose::make(op->sym, src, dims, std::move(body));
}
};
set_result(lift_decl_invariants(body, op->sym, make_transpose));
}
void visit(const clone_buffer* op) override {
// Because we disallow shadowing (i.e. mutating buffers in place), clone_buffer can always be removed :)
// Essentially, every operation is also a clone here.
var src = visit_symbol(op->src);
auto s = set_value_in_scope(vars, op->sym, expr_info::substitution(variable::make(src)));
set_result(mutate(op->body));
}
void visit(const check* op) override {
expr_info c_info;
expr c = mutate_boolean(op->condition, &c_info);
if (!c.defined()) {
set_result(stmt());
} else if (auto proven = attempt_to_prove(c_info.bounds)) {
if (*proven) {
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)));
}
}
using node_mutator::visit;
};
} // namespace
expr simplify(const expr& e, const bounds_map& bounds, const alignment_map& alignment) {
return simplifier(bounds, alignment).mutate(e, nullptr);
}
stmt simplify(const stmt& s, const bounds_map& bounds, const alignment_map& alignment) {
scoped_trace trace("simplify");
return simplifier(bounds, alignment).mutate(s);
}
interval_expr simplify(const interval_expr& e, const bounds_map& bounds, const alignment_map& alignment) {
simplifier s(bounds, alignment);
return s.mutate(e);
}
interval_expr bounds_of(const expr& x, const bounds_map& expr_bounds, const alignment_map& alignment) {
scoped_trace trace("bounds_of");
simplifier s(expr_bounds, alignment);
simplifier::expr_info result;
s.mutate(x, &result);
return result.bounds;
}
interval_expr bounds_of(const interval_expr& x, const bounds_map& expr_bounds, const alignment_map& alignment) {
if (deep_is_point(x)) {
return bounds_of(x.min, expr_bounds);
} else {
scoped_trace trace("bounds_of");
simplifier s(expr_bounds, alignment);
simplifier::expr_info info_of_min, info_of_max;
s.mutate(x, &info_of_min, &info_of_max);
return {
simplify(static_cast<const class min*>(nullptr), info_of_min.bounds.min, info_of_max.bounds.min),
simplify(static_cast<const class max*>(nullptr), info_of_min.bounds.max, info_of_max.bounds.max),
};
}
}
namespace {
class constant_evaluator : public node_mutator {
// > 0 -> we are looking for an upper bound
// == 0 -> we are constant folding
// < 0 -> we are looking for a lower bound
int sign = 0;
bool constant_required = true;
public:
constant_evaluator(bool constant_required = true) : constant_required(constant_required) {}
using node_mutator::mutate;
expr mutate(const expr& x, int sign) {
int old_sign = this->sign;
this->sign = sign;
expr result = mutate(x);
this->sign = old_sign;
return result;
}
void visit(const variable* op) override { set_result(constant_required ? expr() : expr(op)); }
template <typename T>
void visit_min_max(const T* op, bool take_constant) {
expr a = mutate(op->a);
expr b = mutate(op->b);
auto ca = as_constant(a);
auto cb = as_constant(b);
if (ca && cb) {
set_result(make_binary<T>(*ca, *cb));
} else if (take_constant && ca) {
set_result(std::move(a));
} else if (take_constant && cb) {
set_result(std::move(b));
} else if (!(a.defined() && b.defined())) {
set_result(expr());
} else if (a.same_as(op->a) && b.same_as(op->b)) {
set_result(op);
} else {
set_result(T::make(std::move(a), std::move(b)));
}
}
void visit(const class min* op) override { visit_min_max(op, /*take_constant=*/sign > 0); }
void visit(const class max* op) override { visit_min_max(op, /*take_constant=*/sign < 0); }
template <typename T>
void set_binary_result(const T* op, expr a, expr b) {
auto ca = as_constant(a);
auto cb = as_constant(b);
if (ca && cb) {
set_result(make_or_eval_binary<T>(*ca, *cb));
} else if (!(a.defined() && b.defined())) {
set_result(expr());
} else if (a.same_as(op->a) && b.same_as(op->b)) {
set_result(op);
} else {
set_result(T::make(std::move(a), std::move(b)));
}
}
void visit(const add* op) override { set_binary_result(op, mutate(op->a), mutate(op->b, sign)); }
void visit(const sub* op) override { set_binary_result(op, mutate(op->a), mutate(op->b, -sign)); }
static int sign_of(const expr& x) {
if (is_positive(x)) return 1;
if (is_negative(x)) return -1;
return 0;
}
template <typename T>
void visit_mul_div(const T* op) {
expr a = mutate(op->a, 0);
expr b = mutate(op->b, 0);
if (sign == 0 || is_constant(a, 0) || is_constant(b, 0) || (as_constant(a) && as_constant(b))) {
set_binary_result(op, std::move(a), std::move(b));
return;
}
// When we multiply by a negative number, we need to flip whether we are looking for an upper or lower bound.
int sign_a = sign_of(a);
int sign_b = sign_of(b);
// TODO: We should be able to handle the numerator of div too, it's just tricky.
if (std::is_same<T, mul>::value && sign_a != 0) {
set_binary_result(op, std::move(a), mutate(op->b, sign * sign_a));
} else if (sign_b != 0) {
set_binary_result(op, mutate(op->a, sign * sign_b), std::move(b));
} else if (!(a.defined() && b.defined())) {
set_result(expr());
} else {
set_result(op);
}
}
void visit(const mul* op) override { visit_mul_div(op); }
void visit(const div* op) override { visit_mul_div(op); }
void visit(const mod* op) override {
if (sign == 0) {
set_binary_result(op, mutate(op->a), mutate(op->b));
} else if (sign < 0) {
// We know that 0 <= a % b < upper_bound(abs(b)). We might be able to do better if a is constant, but even that is
// not easy, because an upper bound of a is not necessarily an upper bound of a % b.
set_result(expr(0));
} else {
expr equiv = max(0, max(-op->b, op->b) - 1);
expr result = mutate(equiv);
if (!equiv.same_as(result)) {
set_result(std::move(result));
} else if (constant_required) {
set_result(expr());
} else {
set_result(op);
}
}
}
template <typename T>
void visit_logical(const T* op) {
if (sign == 0) {
set_binary_result(op, mutate(op->a), mutate(op->b));
} else {
// If we don't know anything about a logical op, the result is either 0 or 1.
set_result(expr(sign < 0 ? 0 : 1));
}
}
void visit(const equal* op) override { visit_logical(op); }
void visit(const not_equal* op) override { visit_logical(op); }
template <typename T>
void visit_less(const T* op) {
// This is a constant version of that found in bounds_of_less:
// - For a lower bound, we want to know if this can ever be false, so we want the upper bound of the lhs and the
// lower bound of the rhs.
// - For an upper bound, we want to know if this can ever be true, so we want the lower bound of the lhs and the
// upper bound of the rhs.
expr a = mutate(op->a, -sign);
expr b = mutate(op->b);
auto ca = as_constant(a);
auto cb = as_constant(b);
if (ca && cb) {
set_result(make_binary<T>(*ca, *cb));
} else if (sign != 0) {
// If we don't know anything about a logical op, the result is either 0 or 1.
set_result(expr(sign < 0 ? 0 : 1));
} else if (!a.defined() || !b.defined()) {
set_result(expr());
} else if (a.same_as(op->a) && b.same_as(op->b)) {
set_result(expr(op));
} else {
set_result(T::make(std::move(a), std::move(b)));
}
}
void visit(const less* op) override { visit_less(op); }
void visit(const less_equal* op) override { visit_less(op); }
template <typename T>
void visit_logical_and_or(const T* op, int new_sign) {
expr a = strip_boolean(mutate(op->a, new_sign));
expr b = strip_boolean(mutate(op->b, new_sign));
if (as_constant(b)) std::swap(a, b);
if (auto ca = as_constant(a)) {
if (*ca) {
set_result(boolean(std::is_same<T, logical_and>::value ? std::move(b) : std::move(a)));
} else {
set_result(boolean(std::is_same<T, logical_and>::value ? std::move(a) : std::move(b)));
}
} else if (sign != 0) {
// If we don't know anything about a logical op, the result is either 0 or 1.
set_result(expr(sign < 0 ? 0 : 1));
} else if (!a.defined() || !b.defined()) {
set_result(expr());
} else if (a.same_as(op->a) && b.same_as(op->b)) {
set_result(expr(op));
} else {
set_result(T::make(std::move(a), std::move(b)));
}
}
// We can recursively look for bounds if:
// - We're looking for the upper bound of &&, because if either operand is definitely false, the result is false.
// - We're looking for the lower bound of ||, because if either operand is definitely true, the result is true.
// - We're constant folding.
void visit(const logical_and* op) override { visit_logical_and_or(op, std::min(sign, 0)); }
void visit(const logical_or* op) override { visit_logical_and_or(op, std::max(sign, 0)); }
void visit(const logical_not* op) override {
expr a = strip_boolean(mutate(op->a, -sign));
if (auto ca = as_constant(a)) {
set_result(*ca != 0 ? 0 : 1);
} else if (sign != 0) {
set_result(expr(sign < 0 ? 0 : 1));
} else if (!a.defined()) {
set_result(expr());
} else if (a.same_as(op->a)) {
set_result(expr(op));
} else {
set_result(logical_not::make(std::move(a)));
}
}
void visit(const class select* op) override {
expr c = strip_boolean(mutate(op->condition, 0));
if (auto cc = as_constant(c)) {
set_result(mutate(*cc ? op->true_value : op->false_value));
return;
}
expr t = mutate(op->true_value);
expr f = mutate(op->false_value);
auto ct = as_constant(t);
auto cf = as_constant(f);
if (sign < 0 && ct && cf) {
set_result(expr(std::min(*ct, *cf)));
} else if (sign > 0 && ct && cf) {
set_result(expr(std::max(*ct, *cf)));
} else if (!c.defined() || !t.defined() || !f.defined()) {
set_result(expr());
} else if (c.same_as(op->condition) && t.same_as(op->true_value) && f.same_as(op->false_value)) {
set_result(op);
} else {
set_result(select::make(std::move(c), std::move(t), std::move(f)));
}
}
void visit(const call* op) override {
if (op->intrinsic == intrinsic::abs) {
assert(op->args.size() == 1);
expr equiv = max(0, max(op->args[0], -op->args[0]));
expr result = mutate(equiv);
if (!equiv.same_as(result)) {
set_result(std::move(result));
return;
}
} else if (op->intrinsic == intrinsic::and_then || op->intrinsic == intrinsic::or_else) {
assert(op->args.size() == 2);
// We can recursively look for bounds if:
// - We're looking for the upper bound of &&, because if either operand is definitely false, the result is false.
// - We're looking for the lower bound of ||, because if either operand is definitely true, the result is true.
// - We're constant folding.
const bool is_and = op->intrinsic == intrinsic::and_then;
const int new_sign = is_and ? std::min(sign, 0) : std::max(sign, 0);
expr a = strip_boolean(mutate(op->args[0], new_sign));
expr b = strip_boolean(mutate(op->args[1], new_sign));
if (auto ca = as_constant(a)) {
if (*ca) {
set_result(boolean(is_and ? std::move(b) : std::move(a)));
} else {
set_result(boolean(is_and ? std::move(a) : std::move(b)));
}
} else if (auto cb = as_constant(b)) {
if (*cb) {
set_result(boolean(is_and ? std::move(a) : std::move(b)));
} else {
set_result(boolean(is_and ? std::move(b) : std::move(a)));
}
} else if (sign != 0) {
// If we don't know anything about a logical op, the result is either 0 or 1.
set_result(expr(sign < 0 ? 0 : 1));
} else if (!a.defined() || !b.defined()) {
set_result(expr());
} else if (a.same_as(op->args[0]) && b.same_as(op->args[1])) {
set_result(expr(op));
} else {
set_result(call::make(op->intrinsic, {std::move(a), std::move(b)}));
}
return;
}
if (constant_required) {
set_result(expr());
} else if (sign == 0) {
node_mutator::visit(op);
} else {
set_result(op);
}
}
};
} // namespace
bool can_evaluate(intrinsic fn) {
switch (fn) {
case intrinsic::abs:
case intrinsic::and_then:
case intrinsic::or_else: return true;
default: return false;
}
}
expr constant_lower_bound(const expr& x) { return constant_evaluator(false).mutate(x, -1); }
expr constant_upper_bound(const expr& x) { return constant_evaluator(false).mutate(x, 1); }
std::optional<index_t> evaluate_constant(const expr& x) { return as_constant(constant_evaluator().mutate(x, 0)); }
std::optional<index_t> evaluate_constant_lower_bound(const expr& x) {
return as_constant(constant_evaluator().mutate(x, -1));
}
std::optional<index_t> evaluate_constant_upper_bound(const expr& x) {
return as_constant(constant_evaluator().mutate(x, 1));
}
std::optional<bool> attempt_to_prove(
const expr& condition, const bounds_map& expr_bounds, const alignment_map& alignment) {
simplifier s(expr_bounds, alignment);
return s.attempt_to_prove(condition);
}
bool prove_true(const expr& condition, const bounds_map& expr_bounds, const alignment_map& alignment) {
simplifier s(expr_bounds, alignment);
return s.prove_true(condition);
}
bool prove_false(const expr& condition, const bounds_map& expr_bounds, const alignment_map& alignment) {
simplifier s(expr_bounds, alignment);
return s.prove_false(condition);
}
} // namespace slinky