| #include "evaluate.h" |
| |
| #include <cassert> |
| #include <iostream> |
| |
| #include "print.h" |
| #include "simplify.h" |
| #include "substitute.h" |
| |
| namespace slinky { |
| |
| bool can_evaluate(intrinsic fn) { |
| switch (fn) { |
| case intrinsic::abs: return true; |
| default: return false; |
| } |
| } |
| |
| void dump_context_for_expr( |
| std::ostream& s, const symbol_map<index_t>& ctx, const expr& deps_of, const node_context* symbols = nullptr) { |
| for (symbol_id i = 0; i < ctx.size(); ++i) { |
| std::string name = symbols ? symbols->name(i) : "<" + std::to_string(i) + ">"; |
| if (!deps_of.defined() || depends_on_variable(deps_of, i)) { |
| if (ctx.contains(i)) { |
| s << " " << name << " = " << *ctx.lookup(i) << std::endl; |
| } else { |
| s << " " << name << " = <>" << std::endl; |
| } |
| } else if (!deps_of.defined() || depends_on_buffer(deps_of, i)) { |
| if (ctx.contains(i)) { |
| const raw_buffer* buf = reinterpret_cast<const raw_buffer*>(*ctx.lookup(i)); |
| s << " " << name << " = {base=" << buf->base << ", elem_size=" << buf->elem_size << ", dims={"; |
| for (std::size_t d = 0; d < buf->rank; ++d) { |
| const dim& dim = buf->dims[d]; |
| s << "{min=" << dim.min() << ", max=" << dim.max() << ", extent=" << dim.extent() |
| << ", stride=" << dim.stride(); |
| if (dim.fold_factor() > 0) { |
| s << ", fold_factor=" << dim.fold_factor(); |
| } |
| s << "}"; |
| if (d + 1 < buf->rank) { |
| s << ","; |
| } |
| } |
| s << "}" << std::endl; |
| } |
| } |
| } |
| } |
| |
| // TODO(https://github.com/dsharlet/slinky/issues/2): I think the T::accept/node_visitor::visit |
| // overhead (two virtual function calls per node) might be significant. This could be implemented |
| // as a switch statement instead. |
| class evaluator : public node_visitor { |
| public: |
| index_t result = 0; |
| eval_context& context; |
| |
| evaluator(eval_context& context) : context(context) {} |
| |
| // Skip the visitor pattern (two virtual function calls) for some frequently used node types. |
| void visit(const expr& x) { |
| switch (x.type()) { |
| case node_type::variable: visit(reinterpret_cast<const variable*>(x.get())); return; |
| case node_type::constant: visit(reinterpret_cast<const constant*>(x.get())); return; |
| default: x.accept(this); |
| } |
| } |
| |
| // Assume `e` is defined, evaluate it and return the result. |
| index_t eval_expr(const expr& e) { |
| visit(e); |
| index_t r = result; |
| result = 0; |
| return r; |
| } |
| |
| // If `e` is defined, evaluate it and return the result. Otherwise, return default `def`. |
| index_t eval_expr(const expr& e, index_t def) { |
| if (e.defined()) { |
| return eval_expr(e); |
| } else { |
| return def; |
| } |
| } |
| |
| void visit(const variable* v) override { |
| auto value = context.lookup(v->name); |
| assert(value); |
| result = *value; |
| } |
| |
| void visit(const wildcard* w) override { |
| // Maybe evaluating this should just be an error. |
| auto value = context.lookup(w->name); |
| assert(value); |
| result = *value; |
| } |
| |
| void visit(const constant* c) override { result = c->value; } |
| |
| template <typename T> |
| void visit_let(const T* l) { |
| auto set_value = set_value_in_scope(context, l->name, eval_expr(l->value)); |
| l->body.accept(this); |
| } |
| |
| void visit(const let* l) override { visit_let(l); } |
| void visit(const let_stmt* l) override { visit_let(l); } |
| |
| void visit(const add* x) override { result = eval_expr(x->a) + eval_expr(x->b); } |
| void visit(const sub* x) override { result = eval_expr(x->a) - eval_expr(x->b); } |
| void visit(const mul* x) override { result = eval_expr(x->a) * eval_expr(x->b); } |
| void visit(const div* x) override { result = euclidean_div(eval_expr(x->a), eval_expr(x->b)); } |
| void visit(const mod* x) override { result = euclidean_mod(eval_expr(x->a), eval_expr(x->b)); } |
| void visit(const class min* x) override { result = std::min(eval_expr(x->a), eval_expr(x->b)); } |
| void visit(const class max* x) override { result = std::max(eval_expr(x->a), eval_expr(x->b)); } |
| void visit(const equal* x) override { result = eval_expr(x->a) == eval_expr(x->b); } |
| void visit(const not_equal* x) override { result = eval_expr(x->a) != eval_expr(x->b); } |
| void visit(const less* x) override { result = eval_expr(x->a) < eval_expr(x->b); } |
| void visit(const less_equal* x) override { result = eval_expr(x->a) <= eval_expr(x->b); } |
| void visit(const logical_and* x) override { result = eval_expr(x->a) != 0 && eval_expr(x->b) != 0; } |
| void visit(const logical_or* x) override { result = eval_expr(x->a) != 0 || eval_expr(x->b) != 0; } |
| void visit(const logical_not* x) override { result = eval_expr(x->x) == 0; } |
| |
| void visit(const class select* x) override { |
| if (eval_expr(x->condition)) { |
| result = eval_expr(x->true_value); |
| } else { |
| result = eval_expr(x->false_value); |
| } |
| } |
| |
| index_t eval_buffer_metadata(const call* x) { |
| assert(x->args.size() == 1); |
| raw_buffer* buf = reinterpret_cast<raw_buffer*>(eval_expr(x->args[0])); |
| assert(buf); |
| switch (x->intrinsic) { |
| case intrinsic::buffer_rank: return buf->rank; |
| case intrinsic::buffer_elem_size: return buf->elem_size; |
| case intrinsic::buffer_base: return reinterpret_cast<index_t>(buf->base); |
| case intrinsic::buffer_size_bytes: return buf->size_bytes(); |
| default: std::abort(); |
| } |
| } |
| |
| index_t eval_dim_metadata(const call* x) { |
| assert(x->args.size() == 2); |
| raw_buffer* buffer = reinterpret_cast<raw_buffer*>(eval_expr(x->args[0])); |
| assert(buffer); |
| index_t d = eval_expr(x->args[1]); |
| assert(d < static_cast<index_t>(buffer->rank)); |
| const slinky::dim& dim = buffer->dim(d); |
| switch (x->intrinsic) { |
| case intrinsic::buffer_min: return dim.min(); |
| case intrinsic::buffer_max: return dim.max(); |
| case intrinsic::buffer_extent: return dim.extent(); |
| case intrinsic::buffer_stride: return dim.stride(); |
| case intrinsic::buffer_fold_factor: return dim.fold_factor(); |
| default: std::abort(); |
| } |
| } |
| |
| void* eval_buffer_at(const call* x) { |
| assert(x->args.size() >= 1); |
| raw_buffer* buf = reinterpret_cast<raw_buffer*>(eval_expr(x->args[0])); |
| void* result = buf->base; |
| assert(x->args.size() <= buf->rank + 1); |
| for (std::size_t d = 0; d < x->args.size() - 1; ++d) { |
| if (x->args[d + 1].defined()) { |
| result = offset_bytes(result, buf->dims[d].flat_offset_bytes(eval_expr(x->args[d + 1]))); |
| } |
| } |
| return result; |
| } |
| |
| void visit(const call* x) override { |
| switch (x->intrinsic) { |
| case intrinsic::positive_infinity: std::cerr << "Cannot evaluate positive_infinity" << std::endl; std::abort(); |
| case intrinsic::negative_infinity: std::cerr << "Cannot evaluate negative_infinity" << std::endl; std::abort(); |
| case intrinsic::indeterminate: std::cerr << "Cannot evaluate indeterminate" << std::endl; std::abort(); |
| |
| case intrinsic::abs: |
| assert(x->args.size() == 1); |
| result = std::abs(eval_expr(x->args[0])); |
| return; |
| |
| case intrinsic::buffer_rank: |
| case intrinsic::buffer_elem_size: |
| case intrinsic::buffer_base: |
| case intrinsic::buffer_size_bytes: |
| result = eval_buffer_metadata(x); |
| return; |
| |
| case intrinsic::buffer_min: |
| case intrinsic::buffer_max: |
| case intrinsic::buffer_extent: |
| case intrinsic::buffer_stride: |
| case intrinsic::buffer_fold_factor: |
| result = eval_dim_metadata(x); |
| return; |
| |
| case intrinsic::buffer_at: |
| result = reinterpret_cast<index_t>(eval_buffer_at(x)); |
| return; |
| default: |
| std::cerr << "Unknown intrinsic: " << x->intrinsic << std::endl; |
| std::abort(); |
| } |
| } |
| |
| void visit(const block* b) override { |
| if (result == 0) b->a.accept(this); |
| if (result == 0) b->b.accept(this); |
| } |
| |
| void visit(const loop* l) override { |
| index_t min = eval_expr(l->bounds.min); |
| index_t max = eval_expr(l->bounds.max); |
| // TODO(https://github.com/dsharlet/slinky/issues/3): We don't get a reference to context[l->name] here |
| // because the context could grow and invalidate the reference. This could be fixed by having evaluate |
| // fully traverse the expression to find the max symbol_id, and pre-allocate the context up front. It's |
| // not clear this optimization is necessary yet. |
| std::optional<index_t> old_value = context[l->name]; |
| for (index_t i = min; result == 0 && i <= max; ++i) { |
| context[l->name] = i; |
| l->body.accept(this); |
| } |
| context[l->name] = old_value; |
| } |
| |
| void visit(const if_then_else* n) override { |
| if (eval_expr(n->condition)) { |
| n->true_body.accept(this); |
| } else if (n->false_body.defined()) { |
| n->false_body.accept(this); |
| } |
| } |
| |
| void visit(const call_func* n) override { |
| index_t* scalars = reinterpret_cast<index_t*>(alloca(n->scalar_args.size() * sizeof(index_t))); |
| for (std::size_t i = 0; i < n->scalar_args.size(); ++i) { |
| scalars[i] = eval_expr(n->scalar_args[i]); |
| } |
| |
| raw_buffer** buffers = reinterpret_cast<raw_buffer**>(alloca(n->buffer_args.size() * sizeof(raw_buffer*))); |
| for (std::size_t i = 0; i < n->buffer_args.size(); ++i) { |
| buffers[i] = reinterpret_cast<raw_buffer*>(*context.lookup(n->buffer_args[i])); |
| } |
| |
| std::span<const index_t> scalars_span(scalars, n->scalar_args.size()); |
| std::span<raw_buffer*> buffers_span(buffers, n->buffer_args.size()); |
| result = n->target(scalars_span, buffers_span); |
| if (result) { |
| if (context.call_failed) { |
| context.call_failed(n); |
| } else { |
| std::cerr << "call_func failed: " << stmt(n) << "->" << result << std::endl; |
| std::abort(); |
| } |
| } |
| } |
| |
| void visit(const allocate* n) override { |
| std::size_t rank = n->dims.size(); |
| // Allocate a buffer with space for its dims on the stack. |
| char* storage = reinterpret_cast<char*>(alloca(sizeof(raw_buffer) + sizeof(dim) * rank)); |
| raw_buffer* buffer = reinterpret_cast<raw_buffer*>(&storage[0]); |
| buffer->elem_size = n->elem_size; |
| buffer->rank = rank; |
| buffer->dims = reinterpret_cast<dim*>(&storage[sizeof(raw_buffer)]); |
| |
| for (std::size_t i = 0; i < rank; ++i) { |
| slinky::dim& dim = buffer->dim(i); |
| dim.set_bounds(eval_expr(n->dims[i].min()), eval_expr(n->dims[i].max())); |
| dim.set_stride(eval_expr(n->dims[i].stride)); |
| dim.set_fold_factor(eval_expr(n->dims[i].fold_factor)); |
| } |
| |
| if (n->storage == memory_type::stack) { |
| buffer->base = alloca(buffer->size_bytes()); |
| } else { |
| assert(n->storage == memory_type::heap); |
| buffer->allocation = nullptr; |
| if (context.allocate) { |
| assert(context.free); |
| context.allocate(n->name, buffer); |
| } else { |
| buffer->allocate(); |
| } |
| } |
| |
| auto set_buffer = set_value_in_scope(context, n->name, reinterpret_cast<index_t>(buffer)); |
| n->body.accept(this); |
| |
| if (n->storage == memory_type::heap) { |
| if (context.free) { |
| assert(context.allocate); |
| context.free(n->name, buffer); |
| } else { |
| buffer->free(); |
| } |
| } |
| } |
| |
| void visit(const make_buffer* n) override { |
| std::size_t rank = n->dims.size(); |
| // Allocate a buffer with space for its dims on the stack. |
| char* storage = reinterpret_cast<char*>(alloca(sizeof(raw_buffer) + sizeof(dim) * rank)); |
| raw_buffer* buffer = reinterpret_cast<raw_buffer*>(&storage[0]); |
| buffer->elem_size = eval_expr(n->elem_size); |
| buffer->base = reinterpret_cast<void*>(eval_expr(n->base)); |
| buffer->rank = rank; |
| buffer->dims = reinterpret_cast<dim*>(&storage[sizeof(raw_buffer)]); |
| |
| for (std::size_t i = 0; i < rank; ++i) { |
| slinky::dim& dim = buffer->dim(i); |
| dim.set_bounds(eval_expr(n->dims[i].min()), eval_expr(n->dims[i].max())); |
| dim.set_stride(eval_expr(n->dims[i].stride)); |
| dim.set_fold_factor(eval_expr(n->dims[i].fold_factor)); |
| } |
| |
| auto set_buffer = set_value_in_scope(context, n->name, reinterpret_cast<index_t>(buffer)); |
| n->body.accept(this); |
| } |
| |
| void visit(const crop_buffer* n) override { |
| raw_buffer* buffer = reinterpret_cast<raw_buffer*>(*context.lookup(n->name)); |
| |
| struct range { |
| index_t min; |
| index_t extent; |
| }; |
| |
| std::size_t crop_rank = n->bounds.size(); |
| range* old_bounds = reinterpret_cast<range*>(alloca(sizeof(range) * crop_rank)); |
| |
| index_t offset = 0; |
| for (std::size_t d = 0; d < crop_rank; ++d) { |
| slinky::dim& dim = buffer->dims[d]; |
| old_bounds[d].min = dim.min(); |
| old_bounds[d].extent = dim.extent(); |
| |
| // Allow these expressions to be undefined, and if so, they default to their existing values. |
| index_t min = eval_expr(n->bounds[d].min, dim.min()); |
| index_t max = eval_expr(n->bounds[d].max, dim.max()); |
| offset += dim.flat_offset_bytes(min); |
| |
| dim.set_bounds(min, max); |
| } |
| |
| void* old_base = buffer->base; |
| buffer->base = offset_bytes(buffer->base, offset); |
| |
| n->body.accept(this); |
| |
| buffer->base = old_base; |
| for (std::size_t d = 0; d < crop_rank; ++d) { |
| slinky::dim& dim = buffer->dims[d]; |
| dim.set_min_extent(old_bounds[d].min, old_bounds[d].extent); |
| } |
| } |
| |
| void visit(const crop_dim* n) override { |
| raw_buffer* buffer = reinterpret_cast<raw_buffer*>(*context.lookup(n->name)); |
| slinky::dim& dim = buffer->dims[n->dim]; |
| |
| void* old_base = buffer->base; |
| index_t old_min = dim.min(); |
| index_t old_extent = dim.extent(); |
| |
| index_t min = eval_expr(n->bounds.min); |
| buffer->base = offset_bytes(buffer->base, dim.flat_offset_bytes(min)); |
| if (n->bounds.min.same_as(n->bounds.max)) { |
| // Crops to a single element are common, we can optimize them a little bit by re-using the min |
| dim.set_point(min); |
| } else { |
| dim.set_bounds(min, eval_expr(n->bounds.max)); |
| } |
| |
| n->body.accept(this); |
| |
| buffer->base = old_base; |
| dim.set_min_extent(old_min, old_extent); |
| } |
| |
| void visit(const slice_buffer* n) override { |
| raw_buffer* buffer = reinterpret_cast<raw_buffer*>(*context.lookup(n->name)); |
| |
| // The rank of the result is equal to the current rank, less any sliced dimensions. |
| std::size_t old_rank = buffer->rank; |
| dim* old_dims = buffer->dims; |
| |
| // TODO: If we really care about stack usage here, we could find the number of dimensions we actually need first. |
| buffer->dims = reinterpret_cast<dim*>(alloca(sizeof(dim) * old_rank)); |
| |
| buffer->rank = 0; |
| index_t offset = 0; |
| for (std::size_t d = 0; d < old_rank; ++d) { |
| if (d < n->at.size() && n->at[d].defined()) { |
| offset += old_dims[d].flat_offset_bytes(eval_expr(n->at[d])); |
| } else { |
| buffer->dims[buffer->rank++] = old_dims[d]; |
| } |
| } |
| |
| void* old_base = buffer->base; |
| buffer->base = offset_bytes(buffer->base, offset); |
| |
| n->body.accept(this); |
| |
| buffer->base = old_base; |
| buffer->rank = old_rank; |
| buffer->dims = old_dims; |
| } |
| |
| void visit(const slice_dim* n) override { |
| raw_buffer* buffer = reinterpret_cast<raw_buffer*>(*context.lookup(n->name)); |
| |
| // The rank of the result is equal to the current rank, less any sliced dimensions. |
| dim* old_dims = buffer->dims; |
| |
| buffer->dims = reinterpret_cast<dim*>(alloca(sizeof(dim) * (buffer->rank - 1))); |
| |
| index_t at = eval_expr(n->at); |
| index_t offset = old_dims[n->dim].flat_offset_bytes(at); |
| void* old_base = buffer->base; |
| buffer->base = offset_bytes(buffer->base, offset); |
| |
| for (int d = 0; d < n->dim; ++d) { |
| buffer->dims[d] = old_dims[d]; |
| } |
| for (int d = n->dim + 1; d < static_cast<int>(buffer->rank); ++d) { |
| buffer->dims[d - 1] = old_dims[d]; |
| } |
| buffer->rank -= 1; |
| |
| n->body.accept(this); |
| |
| buffer->base = old_base; |
| buffer->rank += 1; |
| buffer->dims = old_dims; |
| } |
| |
| void visit(const truncate_rank* n) override { |
| raw_buffer* buffer = reinterpret_cast<raw_buffer*>(*context.lookup(n->name)); |
| |
| std::size_t old_rank = buffer->rank; |
| buffer->rank = n->rank; |
| |
| n->body.accept(this); |
| |
| buffer->rank = old_rank; |
| } |
| |
| void visit(const check* n) override { |
| result = eval_expr(n->condition, 0) != 0 ? 0 : 1; |
| if (result) { |
| if (context.check_failed) { |
| context.check_failed(n->condition); |
| } else { |
| std::cerr << "Check failed: " << n->condition << std::endl; |
| std::cerr << "Context: " << std::endl; |
| dump_context_for_expr(std::cerr, context, n->condition); |
| std::abort(); |
| } |
| } |
| } |
| }; |
| |
| index_t evaluate(const expr& e, eval_context& context) { |
| evaluator eval(context); |
| e.accept(&eval); |
| return eval.result; |
| } |
| |
| index_t evaluate(const stmt& s, eval_context& context) { |
| evaluator eval(context); |
| s.accept(&eval); |
| return eval.result; |
| } |
| |
| index_t evaluate(const expr& e) { |
| eval_context ctx; |
| return evaluate(e, ctx); |
| } |
| |
| index_t evaluate(const stmt& s) { |
| eval_context ctx; |
| return evaluate(s, ctx); |
| } |
| |
| namespace { |
| |
| class validator : public node_visitor { |
| enum variable_state { |
| unknown, |
| pointer, |
| arithmetic, |
| }; |
| |
| const node_context* symbols; |
| |
| struct symbol_info { |
| variable_state state; |
| stmt decl_stmt; |
| expr decl_expr; |
| |
| symbol_info(variable_state state) : state(state) {} |
| symbol_info(variable_state state, stmt s) : state(state), decl_stmt(std::move(s)) {} |
| symbol_info(variable_state state, expr e) : state(state), decl_expr(std::move(e)) {} |
| }; |
| symbol_map<symbol_info> ctx; |
| |
| variable_state state = unknown; |
| |
| void print_symbol_info(const symbol_info& s) { |
| if (s.decl_stmt.defined()) { |
| std::cerr << "Declared by:" << std::endl; |
| print(std::cerr, s.decl_stmt, symbols); |
| } else if (s.decl_expr.defined()) { |
| std::cerr << "Declared by:" << std::endl; |
| print(std::cerr, s.decl_expr, symbols); |
| } else { |
| std::cerr << "Externally defined symbol" << std::endl; |
| } |
| } |
| |
| public: |
| bool error = false; |
| |
| validator(const std::vector<symbol_id>& inputs, const node_context* symbols) : symbols(symbols) { |
| for (symbol_id i : inputs) { |
| ctx[i] = unknown; |
| } |
| } |
| |
| void visit(const variable* x) override { |
| if (!ctx.contains(x->name)) { |
| std::cerr << "Undefined variable "; |
| print(std::cerr, x, symbols); |
| std::cerr << " in context" << std::endl; |
| error = true; |
| } |
| state = unknown; |
| } |
| void visit(const wildcard* x) override { |
| if (!ctx.contains(x->name)) { |
| std::cerr << "Undefined wildcard "; |
| print(std::cerr, x, symbols); |
| std::cerr << std::endl << "In context:" << std::endl; |
| error = true; |
| } |
| } |
| void visit(const constant*) override { state = unknown; } |
| |
| template <typename T> |
| void visit_let(const T* x) { |
| x->value.accept(this); |
| auto s = set_value_in_scope(ctx, x->name, {state, x}); |
| x->body.accept(this); |
| } |
| |
| void visit(const let* x) override { visit_let(x); } |
| |
| void check_arithmetic(const expr& x, bool required = true) { |
| if (error) return; |
| |
| if (x.defined()) { |
| x.accept(this); |
| if (state == pointer) { |
| std::cerr << "Arithmetic on pointer value: "; |
| print(std::cerr, x, symbols); |
| std::cerr << std::endl << "In context:" << std::endl; |
| error = true; |
| } |
| } else if (required) { |
| std::cerr << "Undefined expression in context:" << std::endl; |
| } |
| } |
| |
| void check(const expr& x) { |
| if (error) return; |
| x.accept(this); |
| } |
| |
| template <typename T> |
| void visit_binary_arithmetic(const T* x) { |
| check_arithmetic(x->a); |
| check_arithmetic(x->b); |
| } |
| |
| template <typename T> |
| void visit_binary(const T* x) { |
| check(x->a); |
| check(x->b); |
| } |
| |
| void visit(const add* x) override { visit_binary_arithmetic(x); } |
| void visit(const sub* x) override { visit_binary_arithmetic(x); } |
| void visit(const mul* x) override { visit_binary_arithmetic(x); } |
| void visit(const div* x) override { visit_binary_arithmetic(x); } |
| void visit(const mod* x) override { visit_binary_arithmetic(x); } |
| void visit(const class min* x) override { visit_binary_arithmetic(x); } |
| void visit(const class max* x) override { visit_binary_arithmetic(x); } |
| void visit(const equal* x) override { visit_binary(x); } |
| void visit(const not_equal* x) override { visit_binary(x); } |
| void visit(const less* x) override { visit_binary_arithmetic(x); } |
| void visit(const less_equal* x) override { visit_binary_arithmetic(x); } |
| void visit(const logical_and* x) override { visit_binary(x); } |
| void visit(const logical_or* x) override { visit_binary(x); } |
| void visit(const logical_not* x) override { check(x->x); } |
| void visit(const class select* x) override { |
| check(x->condition); |
| check(x->true_value); |
| check(x->false_value); |
| } |
| |
| void check_pointer(const expr& x) { |
| x.accept(this); |
| if (state == arithmetic) { |
| std::cerr << "Expression " << x << " is arithmetic, expected pointer" << std::endl; |
| std::cerr << std::endl << "In context:" << std::endl; |
| error = true; |
| } |
| } |
| |
| void check_pointer(symbol_id name) { |
| std::optional<symbol_info> state = ctx.lookup(name); |
| if (state && state->state == arithmetic) { |
| std::cerr << "Arithmetic symbol "; |
| print(std::cerr, var(name), symbols); |
| std::cerr << " used as a pointer" << std::endl; |
| print_symbol_info(*state); |
| error = true; |
| } |
| } |
| |
| void visit(const call* x) override { |
| if (error) return; |
| |
| switch (x->intrinsic) { |
| case intrinsic::negative_infinity: |
| case intrinsic::positive_infinity: |
| case intrinsic::indeterminate: |
| std::cerr << "Cannot evaluate " << x->intrinsic << std::endl; |
| error = true; |
| return; |
| |
| case intrinsic::abs: check_arithmetic(x->args[0]); return; |
| |
| case intrinsic::buffer_rank: |
| case intrinsic::buffer_elem_size: |
| case intrinsic::buffer_size_bytes: |
| case intrinsic::buffer_base: // We treat pointers to data as arithmetic |
| if (x->args.size() != 1) { |
| std::cerr << "Wrong number of arguments for buffer intrinsic " << x->intrinsic << std::endl; |
| error = true; |
| return; |
| } |
| check_pointer(x->args[0]); |
| state = arithmetic; |
| return; |
| case intrinsic::buffer_min: |
| case intrinsic::buffer_max: |
| case intrinsic::buffer_stride: |
| case intrinsic::buffer_fold_factor: |
| case intrinsic::buffer_extent: |
| if (x->args.size() != 2) { |
| std::cerr << "Wrong number of arguments for buffer intrinsic " << x->intrinsic << std::endl; |
| error = true; |
| return; |
| } |
| check_pointer(x->args[0]); |
| check_arithmetic(x->args[1]); |
| state = arithmetic; |
| return; |
| |
| case intrinsic::buffer_at: |
| check_pointer(x->args[0]); |
| for (std::size_t i = 1; i < x->args.size(); ++i) { |
| check_arithmetic(x->args[i]); |
| } |
| state = arithmetic; |
| return; |
| } |
| } |
| |
| void visit(const let_stmt* x) override { visit_let(x); } |
| |
| void check(const stmt& s, bool required = true) { |
| if (error) return; |
| |
| if (s.defined()) { |
| s.accept(this); |
| } else if (required) { |
| std::cerr << "Undefined statement " << std::endl; |
| error = true; |
| } |
| } |
| |
| void visit(const block* x) override { |
| check(x->a, /*required=*/false); |
| check(x->b, /*required=*/false); |
| } |
| void visit(const loop* x) override { |
| check_arithmetic(x->bounds.min); |
| check_arithmetic(x->bounds.max); |
| auto s = set_value_in_scope(ctx, x->name, {arithmetic, x}); |
| check(x->body); |
| } |
| void visit(const if_then_else* x) override { |
| check_arithmetic(x->condition); |
| check(x->true_body, /*required=*/false); |
| check(x->false_body, /*required=*/false); |
| } |
| void visit(const call_func* x) override { |
| for (symbol_id b : x->buffer_args) { |
| check_pointer(b); |
| } |
| } |
| |
| void check_arithmetic(const interval_expr& b, bool required = true) { |
| check_arithmetic(b.min, required); |
| check_arithmetic(b.max, required); |
| } |
| void check_arithmetic(const dim_expr& d, bool required = true) { |
| check_arithmetic(d.bounds, required); |
| check_arithmetic(d.stride, required); |
| check_arithmetic(d.fold_factor, required); |
| } |
| |
| void visit(const allocate* x) override { |
| for (const dim_expr& i : x->dims) { |
| check_arithmetic(i); |
| } |
| auto s = set_value_in_scope(ctx, x->name, {pointer, x}); |
| check(x->body); |
| } |
| void visit(const make_buffer* x) override { |
| check_arithmetic(x->base); // We treat pointers to data as arithmetic |
| check_arithmetic(x->elem_size); |
| for (const dim_expr& i : x->dims) { |
| check_arithmetic(i); |
| } |
| auto s = set_value_in_scope(ctx, x->name, {pointer, x}); |
| check(x->body); |
| } |
| void visit(const crop_buffer* x) override { |
| check_pointer(x->name); |
| for (const interval_expr& i : x->bounds) { |
| check_arithmetic(i, /*required=*/false); |
| } |
| check(x->body); |
| } |
| void visit(const crop_dim* x) override { |
| check_pointer(x->name); |
| check_arithmetic(x->bounds); |
| check(x->body); |
| } |
| void visit(const slice_buffer* x) override { |
| check_pointer(x->name); |
| for (const expr& i : x->at) { |
| check_arithmetic(i, /*required=*/false); |
| } |
| check(x->body); |
| } |
| void visit(const slice_dim* x) override { |
| check_pointer(x->name); |
| check_arithmetic(x->at); |
| check(x->body); |
| } |
| void visit(const truncate_rank* x) override { check_pointer(x->name); } |
| void visit(const class check* x) override { check(x->condition); } |
| }; |
| |
| } // namespace |
| |
| bool is_valid(const expr& e, const std::vector<symbol_id>& inputs, const node_context* symbols) { |
| validator v(inputs, symbols); |
| e.accept(&v); |
| return !v.error; |
| } |
| |
| bool is_valid(const stmt& s, const std::vector<symbol_id>& inputs, const node_context* symbols) { |
| validator v(inputs, symbols); |
| s.accept(&v); |
| return !v.error; |
| } |
| |
| } // namespace slinky |