blob: 3a0f15f05f92210733e6e5ba6980b71def515135 [file] [edit]
#ifndef SLINKY_EXPR_H
#define SLINKY_EXPR_H
#include "buffer.h"
#include "ref_count.h"
#include <cstdlib>
#include <functional>
#include <initializer_list>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <vector>
namespace slinky {
using symbol_id = std::size_t;
// We don't want to be doing string lookups in the inner loops. A node_context
// uniquely maps strings to symbol_id.
class node_context {
std::vector<std::string> id_to_name;
public:
// Get the name of a symbol_id.
std::string name(symbol_id i) const;
// Get or insert a new symbol_id for a name.
symbol_id insert(const std::string& name);
symbol_id insert_unique(const std::string& prefix = "_");
std::optional<symbol_id> lookup(const std::string& name) const;
};
enum class node_type {
none,
variable,
wildcard,
constant,
let,
add,
sub,
mul,
div,
mod,
min,
max,
equal,
not_equal,
less,
less_equal,
logical_and,
logical_or,
logical_not,
select,
call,
call_func,
let_stmt,
block,
loop,
if_then_else,
allocate,
make_buffer,
crop_buffer,
crop_dim,
slice_buffer,
slice_dim,
truncate_rank,
check,
};
enum class memory_type {
stack,
heap,
};
enum class intrinsic {
negative_infinity,
positive_infinity,
indeterminate,
abs,
buffer_rank,
buffer_base,
buffer_elem_size,
buffer_size_bytes,
buffer_min,
buffer_max,
buffer_stride,
buffer_fold_factor,
buffer_extent,
buffer_at,
};
class node_visitor;
// The next few classes are the base of the expression (`expr`) and statement (`stmt`) mechanism.
// `base_expr_node` is the base of `expr`s, and always produce an `index_t`-sized result when evaluated.
// `base_stmt_node` is the base of `stmt`s, and do not produce any result.
// Both are immutable.
class base_node : public ref_counted {
public:
base_node(node_type type) : type(type) {}
virtual void accept(node_visitor* v) const = 0;
node_type type;
template <typename T>
const T* as() const {
if (type == T::static_type) {
return reinterpret_cast<const T*>(this);
} else {
return nullptr;
}
}
};
class base_expr_node : public base_node {
public:
base_expr_node(node_type type) : base_node(type) {}
};
class base_stmt_node : public base_node {
public:
base_stmt_node(node_type type) : base_node(type) {}
};
// These next two are just helpers for constructing the type information.
template <typename T>
class expr_node : public base_expr_node {
public:
expr_node() : base_expr_node(T::static_type) {}
};
template <typename T>
class stmt_node : public base_stmt_node {
public:
stmt_node() : base_stmt_node(T::static_type) {}
};
class expr;
expr operator+(expr a, expr b);
expr operator-(expr a, expr b);
expr operator*(expr a, expr b);
expr operator/(expr a, expr b);
expr operator%(expr a, expr b);
// `expr` is an owner of a reference counted pointer to a `base_expr_node`, `stmt` similarly owns a `base_stmt_node`
// pointer. Operations that appear to mutate these objects are actually just reassigning this reference counted pointer.
class expr {
ref_count<const base_expr_node> n_;
node_type type_ = node_type::none;
public:
expr() = default;
expr(const expr&) = default;
expr(expr&&) = default;
expr& operator=(const expr&) = default;
expr& operator=(expr&&) = default;
// Make a new constant expression.
expr(index_t x);
expr(int x) : expr(static_cast<index_t>(x)) {}
// Make an `expr` referencing an existing node.
expr(const base_expr_node* n) : n_(n), type_(n ? n->type : node_type::none) {}
void accept(node_visitor* v) const {
assert(defined());
n_->accept(v);
}
bool defined() const { return n_ != nullptr; }
bool same_as(const expr& other) const { return n_ == other.n_; }
node_type type() const { return type_; }
const base_expr_node* get() const { return n_; }
template <typename T>
const T* as() const {
if (n_ && type_ == T::static_type) {
return reinterpret_cast<const T*>(&*n_);
} else {
return nullptr;
}
}
expr operator-() const { return 0 - *this; }
expr& operator+=(const expr& r) {
*this = *this + r;
return *this;
}
expr& operator-=(const expr& r) {
*this = *this - r;
return *this;
}
expr& operator*=(const expr& r) {
*this = *this * r;
return *this;
}
expr& operator/=(const expr& r) {
*this = *this / r;
return *this;
}
expr& operator%=(const expr& r) {
*this = *this % r;
return *this;
}
};
expr operator==(expr a, expr b);
expr operator!=(expr a, expr b);
expr operator<(expr a, expr b);
expr operator<=(expr a, expr b);
expr operator>(expr a, expr b);
expr operator>=(expr a, expr b);
expr operator&&(expr a, expr b);
expr operator||(expr a, expr b);
expr operator!(expr x);
expr min(expr a, expr b);
expr max(expr a, expr b);
expr clamp(expr x, expr a, expr b);
expr select(expr c, expr t, expr f);
expr min(std::span<expr> x);
expr max(std::span<expr> x);
struct interval_expr {
expr min, max;
interval_expr() {}
explicit interval_expr(const expr& point) : min(point), max(point) {}
interval_expr(expr min, expr max) : min(std::move(min)), max(std::move(max)) {}
bool same_as(const interval_expr& r) { return min.same_as(r.min) && max.same_as(r.max); }
bool is_single_point() const { return min.same_as(max); }
static const interval_expr& all();
static const interval_expr& none();
// An interval_expr x such that x | y == y
static const interval_expr& union_identity();
// An interval_expr x such that x & y == y
static const interval_expr& intersection_identity();
const expr& begin() const { return min; }
expr end() const { return max + 1; }
expr extent() const { return max - min + 1; }
interval_expr& operator*=(const expr& scale);
interval_expr& operator/=(const expr& scale);
interval_expr& operator+=(const expr& offset);
interval_expr& operator-=(const expr& offset);
interval_expr operator*(const expr& scale) const;
interval_expr operator/(const expr& scale) const;
interval_expr operator+(const expr& offset) const;
interval_expr operator-(const expr& offset) const;
interval_expr operator-() const;
// This is the union operator. I don't really like this, but
// I also don't like that I can't name a function `union`.
// It does kinda make sense...
interval_expr& operator|=(const interval_expr& r);
// This is intersection, just to be consistent with union.
interval_expr& operator&=(const interval_expr& r);
interval_expr operator|(const interval_expr& r) const;
interval_expr operator&(const interval_expr& r) const;
};
// Make an interval of the region [begin, end) (like python's range).
inline interval_expr range(expr begin, const expr& end) { return {std::move(begin), end - 1}; }
// Make an interval of the region [min, max].
inline interval_expr bounds(expr min, expr max) { return {std::move(min), std::move(max)}; }
// Make an interval of the region [min, min + extent).
inline interval_expr min_extent(const expr& min, const expr& extent) { return {min, min + extent - 1}; }
// Make a interval of the region [x, x].
inline interval_expr point(const expr& x) { return {x, x}; }
inline interval_expr operator*(const expr& a, const interval_expr& b) { return b * a; }
inline interval_expr operator+(const expr& a, const interval_expr& b) { return b + a; }
// A box is a multidimensional interval.
using box_expr = std::vector<interval_expr>;
box_expr operator|(box_expr a, const box_expr& b);
box_expr operator&(box_expr a, const box_expr& b);
class stmt {
ref_count<const base_stmt_node> n_;
node_type type_ = node_type::none;
public:
stmt() = default;
stmt(const stmt&) = default;
stmt(stmt&&) = default;
stmt(const base_stmt_node* n) : n_(n), type_(n ? n->type : node_type::none) {}
stmt& operator=(const stmt&) = default;
stmt& operator=(stmt&&) = default;
void accept(node_visitor* v) const {
assert(defined());
n_->accept(v);
}
bool defined() const { return n_ != nullptr; }
bool same_as(const stmt& other) const { return n_ == other.n_; }
node_type type() const { return type_; }
const base_stmt_node* get() const { return n_; }
template <typename T>
const T* as() const {
if (type_ == T::static_type) {
return reinterpret_cast<const T*>(&*n_);
} else {
return nullptr;
}
}
};
// Allows lifting a common subexpression `value` out of another expression `body`, by referencing the value by `name`.
class let : public expr_node<let> {
public:
symbol_id name;
expr value;
expr body;
void accept(node_visitor* v) const;
static expr make(symbol_id name, expr value, expr body);
static constexpr node_type static_type = node_type::let;
};
class variable : public expr_node<variable> {
public:
symbol_id name;
void accept(node_visitor* v) const;
static expr make(symbol_id name);
static constexpr node_type static_type = node_type::variable;
};
// Similar to a variable, designed for use in pattern matching. A match with x is only
// accepted if matches(x) returns true.
// TODO(https://github.com/dsharlet/slinky/issues/6): This is pretty ugly. We should be
// able to contain this kind of logic to pattern matching only, it shouldn't be polluting
// the expression mechanism.
class wildcard : public expr_node<wildcard> {
public:
symbol_id name;
std::function<bool(const expr&)> matches;
void accept(node_visitor* v) const;
static expr make(symbol_id name, std::function<bool(const expr&)> matches);
static constexpr node_type static_type = node_type::wildcard;
};
class constant : public expr_node<constant> {
public:
index_t value;
void accept(node_visitor* v) const;
static expr make(index_t value);
static expr make(const void* value);
static constexpr node_type static_type = node_type::constant;
};
#define DECLARE_BINARY_OP(op) \
class op : public expr_node<class op> { \
public: \
expr a, b; \
void accept(node_visitor* v) const; \
static expr make(expr a, expr b); \
static constexpr node_type static_type = node_type::op; \
};
DECLARE_BINARY_OP(add)
DECLARE_BINARY_OP(sub)
DECLARE_BINARY_OP(mul)
DECLARE_BINARY_OP(div)
DECLARE_BINARY_OP(mod)
DECLARE_BINARY_OP(min)
DECLARE_BINARY_OP(max)
DECLARE_BINARY_OP(equal)
DECLARE_BINARY_OP(not_equal)
DECLARE_BINARY_OP(less)
DECLARE_BINARY_OP(less_equal)
DECLARE_BINARY_OP(logical_and)
DECLARE_BINARY_OP(logical_or)
#undef DECLARE_BINARY_OP
class logical_not : public expr_node<logical_not> {
public:
expr x;
void accept(node_visitor* v) const;
static expr make(expr x);
static constexpr node_type static_type = node_type::logical_not;
};
// Similar to the C++ ternary operator. `true_value` or `false_value` are only evaluated when the `condition` is true or
// false, respectively.
class select : public expr_node<class select> {
public:
expr condition;
expr true_value;
expr false_value;
void accept(node_visitor* v) const;
static expr make(expr condition, expr true_value, expr false_value);
static constexpr node_type static_type = node_type::select;
};
class call : public expr_node<call> {
public:
slinky::intrinsic intrinsic;
std::vector<expr> args;
void accept(node_visitor* v) const;
static expr make(slinky::intrinsic i, std::vector<expr> args);
static constexpr node_type static_type = node_type::call;
};
class func;
// Call `target` with a set of `scalar_args` scalars and `buffer_args` buffers.
class call_func : public stmt_node<call_func> {
public:
typedef index_t (*callable_t)(std::span<const index_t>, std::span<raw_buffer*>);
using callable = std::function<index_t(std::span<const index_t>, std::span<raw_buffer*>)>;
callable target;
std::vector<expr> scalar_args;
std::vector<symbol_id> buffer_args;
const func* fn;
void accept(node_visitor* v) const;
static stmt make(callable target, std::vector<expr> scalar_args, std::vector<symbol_id> buffer_args, const func* fn);
static constexpr node_type static_type = node_type::call_func;
};
// Allows lifting a common subexpression `value` out of a statement `body`, by referencing the value by `name`.
class let_stmt : public stmt_node<let_stmt> {
public:
symbol_id name;
expr value;
stmt body;
void accept(node_visitor* v) const;
static stmt make(symbol_id name, expr value, stmt body);
static constexpr node_type static_type = node_type::let_stmt;
};
// A block is a set of two other `stmt`s. Recursively using blocks allows an arbitrarily large set of `stmt`s to be
// defined in sequence.
class block : public stmt_node<block> {
public:
stmt a, b;
void accept(node_visitor* v) const;
static stmt make(stmt a, stmt b);
// Recursively create blocks to contain all of the `stmts`. This may not produce a block at all if `stmts` contains
// only one item.
static stmt make(std::span<stmt> stmts) { return make(stmts.begin(), stmts.end()); }
static stmt make(std::initializer_list<stmt> stmts) { return make(stmts.begin(), stmts.end()); }
template <typename It>
static stmt make(It begin, It end) {
stmt result;
for (It i = begin; i != end; ++i) {
if (result.defined()) {
result = block::make(result, *i);
} else {
result = *i;
}
}
return result;
}
static constexpr node_type static_type = node_type::block;
};
// Runs `body` for each value i in the interval `bounds` with `name` set to i.
class loop : public stmt_node<loop> {
public:
symbol_id name;
interval_expr bounds;
stmt body;
void accept(node_visitor* v) const;
static stmt make(symbol_id name, interval_expr bounds, stmt body);
static constexpr node_type static_type = node_type::loop;
};
// Run `true_body` if `condition` is true, or `false_body` otherwise. Either body can be undefined, indicating that
// nothing should happen in that case.
class if_then_else : public stmt_node<if_then_else> {
public:
expr condition;
stmt true_body;
stmt false_body;
void accept(node_visitor* v) const;
static stmt make(expr condition, stmt true_body, stmt false_body = stmt());
static constexpr node_type static_type = node_type::if_then_else;
};
// A helper containing sub-expressions that describe a dimension of a buffer, corresponding to `dim`.
struct dim_expr {
interval_expr bounds;
expr stride;
expr fold_factor;
const expr& min() const { return bounds.min; }
const expr& max() const { return bounds.max; }
expr extent() const { return bounds.extent(); }
bool same_as(const dim_expr& r) {
return bounds.same_as(r.bounds) && stride.same_as(r.stride) && fold_factor.same_as(r.fold_factor);
}
};
// Allocates memory and creates a buffer pointing to that memory. When control flow exits `body`, the buffer is freed.
// `name` refers to a pointer to a `raw_buffer` object, the fields are initialized by the corresponding expressions in
// this node (`rank` is the size of `dims`).
class allocate : public stmt_node<allocate> {
public:
memory_type storage;
symbol_id name;
std::size_t elem_size;
std::vector<dim_expr> dims;
stmt body;
void accept(node_visitor* v) const;
static stmt make(memory_type storage, symbol_id name, std::size_t elem_size, std::vector<dim_expr> dims, stmt body);
static constexpr node_type static_type = node_type::allocate;
};
// Make a `raw_buffer` object around an existing pointer `base` with fields corresponding to the expressions in this
// node (`rank` is the size of `dims`).
class make_buffer : public stmt_node<make_buffer> {
public:
symbol_id name;
expr base;
expr elem_size;
std::vector<dim_expr> dims;
stmt body;
void accept(node_visitor* v) const;
static stmt make(symbol_id name, expr base, expr elem_size, std::vector<dim_expr> dims, stmt body);
static constexpr node_type static_type = node_type::make_buffer;
};
// For the `body` scope, crops the buffer `name` to `bounds`. If the expressions in `bounds` are undefined, they default
// to their original values in the existing buffer. The rank of the buffer is unchanged. If the size of `bounds` is less
// than the rank, the missing values are considered undefined.
class crop_buffer : public stmt_node<crop_buffer> {
public:
symbol_id name;
std::vector<interval_expr> bounds;
stmt body;
void accept(node_visitor* v) const;
static stmt make(symbol_id name, std::vector<interval_expr> bounds, stmt body);
static constexpr node_type static_type = node_type::crop_buffer;
};
// Similar to `crop_buffer`, but only crops the dimension `dim`.
class crop_dim : public stmt_node<crop_dim> {
public:
symbol_id name;
int dim;
interval_expr bounds;
stmt body;
void accept(node_visitor* v) const;
static stmt make(symbol_id name, int dim, interval_expr bounds, stmt body);
static constexpr node_type static_type = node_type::crop_dim;
};
// For the `body` scope, slices the buffer `name` at the coordinate `at`. The `at` expressions can be undefined,
// indicating that the corresponding dimension is preserved in the sliced buffer. The sliced buffer will have `rank`
// equal to the rank of the existing buffer, less the number of sliced dimensions. If `at` is smaller than the rank
// of the buffer, the missing values are considered undefined.
class slice_buffer : public stmt_node<slice_buffer> {
public:
symbol_id name;
std::vector<expr> at;
stmt body;
void accept(node_visitor* v) const;
static stmt make(symbol_id name, std::vector<expr> at, stmt body);
static constexpr node_type static_type = node_type::slice_buffer;
};
// Similar to `slice_buffer`, but only slices one dimension `dim`. The rank of the result is one less than the original
// buffer.
class slice_dim : public stmt_node<slice_dim> {
public:
symbol_id name;
int dim;
expr at;
stmt body;
void accept(node_visitor* v) const;
static stmt make(symbol_id name, int dim, expr at, stmt body);
static constexpr node_type static_type = node_type::slice_dim;
};
// Within `body`, remove the dimensions of the buffer `name` above `rank`.
class truncate_rank : public stmt_node<truncate_rank> {
public:
symbol_id name;
int rank;
stmt body;
void accept(node_visitor* v) const;
static stmt make(symbol_id name, int rank, stmt body);
static constexpr node_type static_type = node_type::truncate_rank;
};
// Basically an assert.
class check : public stmt_node<check> {
public:
expr condition;
void accept(node_visitor* v) const;
static stmt make(expr condition);
static constexpr node_type static_type = node_type::check;
};
class node_visitor {
public:
virtual ~node_visitor() {}
virtual void visit(const variable*) = 0;
virtual void visit(const wildcard*) = 0;
virtual void visit(const constant*) = 0;
virtual void visit(const let*) = 0;
virtual void visit(const add*) = 0;
virtual void visit(const sub*) = 0;
virtual void visit(const mul*) = 0;
virtual void visit(const div*) = 0;
virtual void visit(const mod*) = 0;
virtual void visit(const class min*) = 0;
virtual void visit(const class max*) = 0;
virtual void visit(const equal*) = 0;
virtual void visit(const not_equal*) = 0;
virtual void visit(const less*) = 0;
virtual void visit(const less_equal*) = 0;
virtual void visit(const logical_and*) = 0;
virtual void visit(const logical_or*) = 0;
virtual void visit(const logical_not*) = 0;
virtual void visit(const class select*) = 0;
virtual void visit(const call*) = 0;
virtual void visit(const let_stmt*) = 0;
virtual void visit(const block*) = 0;
virtual void visit(const loop*) = 0;
virtual void visit(const if_then_else*) = 0;
virtual void visit(const call_func*) = 0;
virtual void visit(const allocate*) = 0;
virtual void visit(const make_buffer*) = 0;
virtual void visit(const crop_buffer*) = 0;
virtual void visit(const crop_dim*) = 0;
virtual void visit(const slice_buffer*) = 0;
virtual void visit(const slice_dim*) = 0;
virtual void visit(const truncate_rank*) = 0;
virtual void visit(const check*) = 0;
};
class recursive_node_visitor : public node_visitor {
public:
virtual void visit(const variable*) override {}
virtual void visit(const wildcard*) override {}
virtual void visit(const constant*) override {}
virtual void visit(const let* x) override {
x->value.accept(this);
x->body.accept(this);
}
template <typename T>
void visit_binary(const T* x) {
x->a.accept(this);
x->b.accept(this);
}
virtual void visit(const add* x) override { visit_binary(x); }
virtual void visit(const sub* x) override { visit_binary(x); }
virtual void visit(const mul* x) override { visit_binary(x); }
virtual void visit(const div* x) override { visit_binary(x); }
virtual void visit(const mod* x) override { visit_binary(x); }
virtual void visit(const class min* x) override { visit_binary(x); }
virtual void visit(const class max* x) override { visit_binary(x); }
virtual void visit(const equal* x) override { visit_binary(x); }
virtual void visit(const not_equal* x) override { visit_binary(x); }
virtual void visit(const less* x) override { visit_binary(x); }
virtual void visit(const less_equal* x) override { visit_binary(x); }
virtual void visit(const logical_and* x) override { visit_binary(x); }
virtual void visit(const logical_or* x) override { visit_binary(x); }
virtual void visit(const logical_not* x) override { x->x.accept(this); }
virtual void visit(const class select* x) override {
x->condition.accept(this);
x->true_value.accept(this);
x->false_value.accept(this);
}
virtual void visit(const call* x) override {
for (const expr& i : x->args) {
if (i.defined()) i.accept(this);
}
}
virtual void visit(const let_stmt* x) override {
x->value.accept(this);
x->body.accept(this);
}
virtual void visit(const block* x) override {
if (x->a.defined()) x->a.accept(this);
if (x->b.defined()) x->b.accept(this);
}
virtual void visit(const loop* x) override {
x->bounds.min.accept(this);
x->bounds.max.accept(this);
x->body.accept(this);
}
virtual void visit(const if_then_else* x) override {
x->condition.accept(this);
if (x->true_body.defined()) x->true_body.accept(this);
if (x->false_body.defined()) x->false_body.accept(this);
}
virtual void visit(const call_func* x) override {
for (const expr& i : x->scalar_args) {
i.accept(this);
}
}
virtual void visit(const allocate* x) override {
for (const dim_expr& i : x->dims) {
i.bounds.min.accept(this);
i.bounds.max.accept(this);
i.stride.accept(this);
i.fold_factor.accept(this);
}
x->body.accept(this);
}
virtual void visit(const make_buffer* x) override {
x->base.accept(this);
x->elem_size.accept(this);
for (const dim_expr& i : x->dims) {
i.bounds.min.accept(this);
i.bounds.max.accept(this);
i.stride.accept(this);
i.fold_factor.accept(this);
}
x->body.accept(this);
}
virtual void visit(const crop_buffer* x) override {
for (const interval_expr& i : x->bounds) {
if (i.min.defined()) i.min.accept(this);
if (i.max.defined()) i.max.accept(this);
}
x->body.accept(this);
}
virtual void visit(const crop_dim* x) override {
x->bounds.min.accept(this);
x->bounds.max.accept(this);
x->body.accept(this);
}
virtual void visit(const slice_buffer* x) override {
for (const expr& i : x->at) {
if (i.defined()) i.accept(this);
}
x->body.accept(this);
}
virtual void visit(const slice_dim* x) override {
x->at.accept(this);
x->body.accept(this);
}
virtual void visit(const truncate_rank* x) override { x->body.accept(this); }
virtual void visit(const check* x) override { x->condition.accept(this); }
};
inline void variable::accept(node_visitor* v) const { v->visit(this); }
inline void wildcard::accept(node_visitor* v) const { v->visit(this); }
inline void constant::accept(node_visitor* v) const { v->visit(this); }
inline void let::accept(node_visitor* v) const { v->visit(this); }
inline void add::accept(node_visitor* v) const { v->visit(this); }
inline void sub::accept(node_visitor* v) const { v->visit(this); }
inline void mul::accept(node_visitor* v) const { v->visit(this); }
inline void div::accept(node_visitor* v) const { v->visit(this); }
inline void mod::accept(node_visitor* v) const { v->visit(this); }
inline void min::accept(node_visitor* v) const { v->visit(this); }
inline void max::accept(node_visitor* v) const { v->visit(this); }
inline void equal::accept(node_visitor* v) const { v->visit(this); }
inline void not_equal::accept(node_visitor* v) const { v->visit(this); }
inline void less::accept(node_visitor* v) const { v->visit(this); }
inline void less_equal::accept(node_visitor* v) const { v->visit(this); }
inline void logical_and::accept(node_visitor* v) const { v->visit(this); }
inline void logical_or::accept(node_visitor* v) const { v->visit(this); }
inline void logical_not::accept(node_visitor* v) const { v->visit(this); }
inline void select::accept(node_visitor* v) const { v->visit(this); }
inline void call::accept(node_visitor* v) const { v->visit(this); }
inline void let_stmt::accept(node_visitor* v) const { v->visit(this); }
inline void block::accept(node_visitor* v) const { v->visit(this); }
inline void loop::accept(node_visitor* v) const { v->visit(this); }
inline void if_then_else::accept(node_visitor* v) const { v->visit(this); }
inline void call_func::accept(node_visitor* v) const { v->visit(this); }
inline void allocate::accept(node_visitor* v) const { v->visit(this); }
inline void make_buffer::accept(node_visitor* v) const { v->visit(this); }
inline void crop_buffer::accept(node_visitor* v) const { v->visit(this); }
inline void crop_dim::accept(node_visitor* v) const { v->visit(this); }
inline void slice_buffer::accept(node_visitor* v) const { v->visit(this); }
inline void slice_dim::accept(node_visitor* v) const { v->visit(this); }
inline void truncate_rank::accept(node_visitor* v) const { v->visit(this); }
inline void check::accept(node_visitor* v) const { v->visit(this); }
// If `x` is a constant, returns the value of the constant, otherwise `nullptr`.
inline const index_t* as_constant(const expr& x) {
const constant* cx = x.as<constant>();
return cx ? &cx->value : nullptr;
}
// If `x` is a variable, returns the `symbol_id` of the variable, otherwise `nullptr`.
inline const symbol_id* as_variable(const expr& x) {
const variable* vx = x.as<variable>();
return vx ? &vx->name : nullptr;
}
// Check if `x` is equal to the constant `value`.
inline bool is_constant(const expr& x, index_t value) {
const constant* cx = x.as<constant>();
return cx ? cx->value == value : false;
}
inline bool is_zero(const expr& x) { return is_constant(x, 0); }
inline bool is_true(const expr& x) {
const constant* cx = x.as<constant>();
return cx ? cx->value != 0 : false;
}
inline bool is_false(const expr& x) { return is_zero(x); }
// Check if `x` is a call to the intrinsic `fn`.
inline bool is_intrinsic(const expr& x, intrinsic fn) {
const call* c = x.as<call>();
return c ? c->intrinsic == fn : false;
}
inline bool is_positive_infinity(const expr& x) { return is_intrinsic(x, intrinsic::positive_infinity); }
inline bool is_negative_infinity(const expr& x) { return is_intrinsic(x, intrinsic::negative_infinity); }
inline bool is_indeterminate(const expr& x) { return is_intrinsic(x, intrinsic::indeterminate); }
inline bool is_infinity(const expr& x) { return is_positive_infinity(x) || is_negative_infinity(x); }
inline bool is_finite(const expr& x) { return !is_infinity(x) && !is_indeterminate(x); }
// Get an expression representing non-numerical constants.
const expr& positive_infinity();
const expr& negative_infinity();
const expr& indeterminate();
inline bool is_positive(const expr& x) {
if (is_positive_infinity(x)) return true;
const index_t* c = as_constant(x);
return c ? *c > 0 : false;
}
inline bool is_non_negative(const expr& x) {
if (is_positive_infinity(x)) return true;
const index_t* c = as_constant(x);
return c ? *c >= 0 : false;
}
inline bool is_negative(const expr& x) {
if (is_negative_infinity(x)) return true;
const index_t* c = as_constant(x);
return c ? *c < 0 : false;
}
inline bool is_non_positive(const expr& x) {
if (is_negative_infinity(x)) return true;
const index_t* c = as_constant(x);
return c ? *c <= 0 : false;
}
// This is an expr-like wrapper for use where only a `variable` expr is allowed.
class var {
expr e_;
public:
var();
var(symbol_id name);
var(node_context& ctx, const std::string& name);
symbol_id name() const {
assert(e_.defined());
return *as_variable(e_);
}
operator const expr&() const { return e_; }
expr operator-() const { return -e_; }
};
expr abs(expr x);
expr buffer_rank(expr buf);
expr buffer_base(expr buf);
expr buffer_elem_size(expr buf);
expr buffer_min(expr buf, expr dim);
expr buffer_max(expr buf, expr dim);
expr buffer_extent(expr buf, expr dim);
expr buffer_stride(expr buf, expr dim);
expr buffer_fold_factor(expr buf, expr dim);
expr buffer_at(expr buf, const std::vector<expr>& at);
expr buffer_at(expr buf, const std::vector<var>& at);
interval_expr buffer_bounds(const expr& buf, const expr& dim);
dim_expr buffer_dim(const expr& buf, const expr& dim);
bool is_buffer_intrinsic(intrinsic fn);
} // namespace slinky
#endif // SLINKY_EXPR_H