diff --git a/python_bindings/halide/src/halide_/PyIROperator.cpp b/python_bindings/halide/src/halide_/PyIROperator.cpp index 55e19786b7cf..a2db9f5d4e35 100644 --- a/python_bindings/halide/src/halide_/PyIROperator.cpp +++ b/python_bindings/halide/src/halide_/PyIROperator.cpp @@ -69,6 +69,29 @@ py::object py_select(const py::args &args) { return py_select_reduce(args); // Otherwise, the value must be a tuple, too. } +// Shared by print and print_when. If the value being printed is a +// Tuple, all of its elements are printed and the print is attached to +// the first element. +template +py::object py_print(const py::args &args, F make_print) { + if (args.empty()) { + throw py::value_error("print() must have at least 1 argument"); + } + if (is_expr(args[0])) { + return py::cast(make_print(collect_print_args(args))); + } + Tuple t = args[0].cast(); + py::tuple rest(args.size() - 1); + for (size_t i = 1; i < args.size(); i++) { + rest[i - 1] = args[i]; + } + std::vector v = t.as_vector(); + std::vector extra = collect_print_args(py::args(rest)); + v.insert(v.end(), extra.begin(), extra.end()); + t[0] = make_print(v); + return py::cast(t); +} + } // namespace void define_operators(py::module &m) { @@ -145,23 +168,35 @@ void define_operators(py::module &m) { m.def("reinterpret", static_cast(&reinterpret)); m.def("cast", static_cast(&cast)); - m.def("print", [](const py::args &args) -> Expr { - return print(collect_print_args(args)); + m.def("print", [](const py::args &args) -> py::object { + return py_print(args, [](const std::vector &v) { return print(v); }); }); m.def( - "print_when", [](const Expr &condition, const py::args &args) -> Expr { - return print_when(condition, collect_print_args(args)); + "print_when", [](const Expr &condition, const py::args &args) -> py::object { + return py_print(args, [&](const std::vector &v) { return print_when(condition, v); }); }, py::arg("condition")); m.def( "require", [](const Expr &condition, const Expr &value, const py::args &args) -> Expr { - auto v = args_to_vector(args); + auto v = collect_print_args(args); v.insert(v.begin(), value); return require(condition, v); }, py::arg("condition"), py::arg("value")); + m.def( + "require", [](const Expr &condition, const Tuple &value, const py::args &args) -> Tuple { + auto v = collect_print_args(args); + v.insert(v.begin(), Expr()); + Tuple result = value; + for (Expr &e : result) { + v[0] = e; + e = require(condition, v); + } + return result; + }, + py::arg("condition"), py::arg("value")); m.def("lerp", &lerp); m.def("popcount", &popcount); @@ -182,11 +217,24 @@ void define_operators(py::module &m) { return Internal::memoize_tag_helper(result, args_to_vector(cache_key_values)); }, py::arg("result")); + m.def( + "memoize_tag", [](const Tuple &result, const py::args &cache_key_values) -> Tuple { + auto v = args_to_vector(cache_key_values); + Tuple tagged = result; + for (Expr &e : tagged) { + e = Internal::memoize_tag_helper(e, v); + } + return tagged; + }, + py::arg("result")); - m.def("likely", &likely); - m.def("likely_if_innermost", &likely_if_innermost); + m.def("likely", static_cast(&likely)); + m.def("likely", static_cast(&likely)); + m.def("likely_if_innermost", static_cast(&likely_if_innermost)); + m.def("likely_if_innermost", static_cast(&likely_if_innermost)); m.def("saturating_cast", static_cast(&saturating_cast)); - m.def("strict_float", &strict_float); + m.def("strict_float", static_cast(&strict_float)); + m.def("strict_float", static_cast(&strict_float)); m.def("scatter", static_cast &)>(&scatter)); m.def("gather", static_cast &)>(&gather)); m.def("extract_bits", static_cast(&extract_bits)); diff --git a/python_bindings/halide/src/halide_/PyTuple.cpp b/python_bindings/halide/src/halide_/PyTuple.cpp index 4f9bc9c97efd..0de56bf16276 100644 --- a/python_bindings/halide/src/halide_/PyTuple.cpp +++ b/python_bindings/halide/src/halide_/PyTuple.cpp @@ -28,6 +28,14 @@ Tuple to_halide_tuple(const py::object &o) { throw py::value_error("Expected an Expr or tuple-of-Expr."); } +namespace { + +py::iterator tuple_iterator(const Tuple &t) { + return py::make_iterator(t.begin(), t.end()); +} + +} // namespace + void define_tuple(py::module &m) { // Halide::Tuple isn't surfaced to the user in Python; // we define it here to allow PyBind to do some automatic @@ -62,6 +70,13 @@ void define_tuple(py::module &m) { .def(py::init([](const std::vector &v) -> Tuple { return Tuple(v); })) + .def("__len__", &Tuple::size) + .def("__getitem__", [](const Tuple &t, size_t i) -> Expr { + if (i >= t.size()) { + throw py::index_error(); + } + return t[i]; + }) .def("__repr__", [](const Tuple &t) -> std::string { std::ostringstream o; o << ""; @@ -71,7 +86,8 @@ void define_tuple(py::module &m) { std::ostringstream o; o << t; return o.str(); - }); + }) + .def("__iter__", &tuple_iterator, py::keep_alive<0, 1>()); py::implicitly_convertible(); diff --git a/python_bindings/halide/test/correctness/iroperator.py b/python_bindings/halide/test/correctness/iroperator.py index 2a59c181eb64..42a6ac8d685a 100644 --- a/python_bindings/halide/test/correctness/iroperator.py +++ b/python_bindings/halide/test/correctness/iroperator.py @@ -127,9 +127,85 @@ def test_minmax(): assert b[4] == 3 +def test_tuple_helpers(): + x = hl.Var("x") + p = hl.Param(hl.Int(32), "p", 1) + + # Helpers that don't imply any math distribute across Tuples. + f = hl.Func("f") + f[x] = hl.select(x < 10, (0, 0), hl.likely((x, x + 1))) + a, b = f.realize([20]) + for i in range(20): + assert a[i] == (0 if i < 10 else i) + assert b[i] == (0 if i < 10 else i + 1) + + # The result is an hl.Tuple, which can be indexed and iterated. + t = hl.likely((x, x + 1)) + assert len(t) == 2 + assert len(list(t)) == 2 + f = hl.Func("f") + f[x] = t[0] + t[1] + a = f.realize([20]) + for i in range(20): + assert a[i] == 2 * i + 1 + + f = hl.Func("f") + f[x] = hl.likely_if_innermost((x, x + 1)) + a, b = f.realize([20]) + for i in range(20): + assert a[i] == i + assert b[i] == i + 1 + + f = hl.Func("f") + f[x] = hl.strict_float((hl.f32(x) + 1.0, hl.f32(x) * 2.0)) + a, b = f.realize([20]) + for i in range(20): + assert a[i] == i + 1 + assert b[i] == i * 2 + + f = hl.Func("f") + f[x] = hl.memoize_tag((x, x + 1), p) + a, b = f.realize([20]) + for i in range(20): + assert a[i] == i + assert b[i] == i + 1 + + f = hl.Func("f") + f[x] = hl.require(p > 0, (x, x + 1), "p was", p) + a, b = f.realize([20]) + for i in range(20): + assert a[i] == i + assert b[i] == i + 1 + + f = hl.Func("f") + f[x] = hl.print((x, x * 2), "at", x) + output = io.StringIO() + with _redirect_stdout(output): + a, b = f.realize([3]) + expected = "0 0 at 0\n1 2 at 1\n2 4 at 2\n" + actual = output.getvalue() + assert expected == actual, f"Expected: {expected}, Actual: {actual}" + for i in range(3): + assert a[i] == i + assert b[i] == i * 2 + + f = hl.Func("f") + f[x] = hl.print_when(x == 1, (x, x * 2), "at", x) + output = io.StringIO() + with _redirect_stdout(output): + a, b = f.realize([3]) + expected = "1 2 at 1\n" + actual = output.getvalue() + assert expected == actual, f"Expected: {expected}, Actual: {actual}" + for i in range(3): + assert a[i] == i + assert b[i] == i * 2 + + if __name__ == "__main__": test_print_expr() test_print_when() + test_tuple_helpers() test_select() test_select_bad_argmax() test_mux() diff --git a/src/Debug.h b/src/Debug.h index 1d2a30939306..b96bf23240b2 100644 --- a/src/Debug.h +++ b/src/Debug.h @@ -13,9 +13,11 @@ namespace Halide { struct Expr; +class Tuple; struct Type; // Forward declare some things from IRPrinter, which we can't include yet. std::ostream &operator<<(std::ostream &stream, const Expr &); +std::ostream &operator<<(std::ostream &stream, const Tuple &); std::ostream &operator<<(std::ostream &stream, const Type &); class Module; diff --git a/src/Derivative.cpp b/src/Derivative.cpp index cef5b9e94e36..504fdf8995e0 100644 --- a/src/Derivative.cpp +++ b/src/Derivative.cpp @@ -319,8 +319,8 @@ void ReverseAccumulationVisitor::propagate_adjoints( vector zeros; Tuple rhs_tuple = func.values(); zeros.reserve(rhs_tuple.size()); - for (int i = 0; i < (int)rhs_tuple.size(); i++) { - zeros.push_back(make_zero(rhs_tuple[i].type())); + for (const Expr &e : rhs_tuple) { + zeros.push_back(make_zero(e.type())); } self_reference_adjoint = Tuple(zeros); self_reference_args.clear(); @@ -393,9 +393,8 @@ void ReverseAccumulationVisitor::propagate_adjoints( // If the pure definition depends on any functions or buffers, // there is no hope since we will overwrite something Tuple rhs_tuple = func.values(); - for (int tuple_id = 0; tuple_id < (int)rhs_tuple.size(); - tuple_id++) { - if (is_calling_function(rhs_tuple[tuple_id], let_var_mapping)) { + for (const Expr &e : rhs_tuple) { + if (is_calling_function(e, let_var_mapping)) { error(); } } @@ -422,9 +421,8 @@ void ReverseAccumulationVisitor::propagate_adjoints( // Checking 2. here: bool all_zero_or_one_self_adjoint = true; - for (int i = 0; i < (int)self_reference_adjoint.size(); i++) { - if (!is_const(self_reference_adjoint[i], 0) && - !is_const(self_reference_adjoint[i], 1)) { + for (const Expr &e : self_reference_adjoint) { + if (!is_const(e, 0) && !is_const(e, 1)) { all_zero_or_one_self_adjoint = false; break; } @@ -449,9 +447,8 @@ void ReverseAccumulationVisitor::propagate_adjoints( } } if (!r.defined()) { - for (int tuple_id = 0; tuple_id < (int)update_tuple.size(); - tuple_id++) { - r = extract_rdom(update_tuple[tuple_id]); + for (const Expr &e : update_tuple) { + r = extract_rdom(e); if (r.defined()) { break; } diff --git a/src/IROperator.cpp b/src/IROperator.cpp index 996c9eb4a469..e8565eb0a1ce 100644 --- a/src/IROperator.cpp +++ b/src/IROperator.cpp @@ -1732,8 +1732,8 @@ Tuple select(const Expr &condition, const Tuple &true_value, const Tuple &false_ return result; } -Expr select(const Expr &condition, const FuncRef &true_value, const FuncRef &false_value) { - return select(condition, (Expr)true_value, (Expr)false_value); +Tuple select(const Expr &condition, const FuncRef &true_value, const FuncRef &false_value) { + return select(condition, Tuple(true_value), Tuple(false_value)); } Expr mux(const Expr &id, const std::vector &values) { @@ -2920,10 +2920,46 @@ Expr likely_if_innermost(Expr e) { {std::move(e)}, Call::PureIntrinsic); } +Tuple likely(const Tuple &t) { + Tuple result = t; + for (Expr &e : result) { + e = likely(e); + } + return result; +} + +Tuple likely(const FuncRef &f) { + return likely(Tuple(f)); +} + +Tuple likely_if_innermost(const Tuple &t) { + Tuple result = t; + for (Expr &e : result) { + e = likely_if_innermost(e); + } + return result; +} + +Tuple likely_if_innermost(const FuncRef &f) { + return likely_if_innermost(Tuple(f)); +} + Expr strict_float(const Expr &e) { return strictify_float(e); } +Tuple strict_float(const Tuple &t) { + Tuple result = t; + for (Expr &e : result) { + e = strict_float(e); + } + return result; +} + +Tuple strict_float(const FuncRef &f) { + return strict_float(Tuple(f)); +} + Expr undef(Type t) { return Call::make(t, Call::undef, std::vector(), diff --git a/src/IROperator.h b/src/IROperator.h index a8fc33f291a9..bd38f5fecae0 100644 --- a/src/IROperator.h +++ b/src/IROperator.h @@ -882,15 +882,14 @@ inline Tuple select(const Expr &c0, const Tuple &v0, const Expr &c1, const Tuple } // @} -/** select applied to FuncRefs (e.g. select(x < 100, f(x), g(x))) is assumed to - * return an Expr. A runtime error is produced if this is applied to - * tuple-valued Funcs. In that case you should explicitly cast the second and - * third args to Tuple to remove the ambiguity. */ +/** select applied to FuncRefs (e.g. select(x < 100, f(x), g(x))) selects + * between all of the values of the Funcs, and returns a Tuple. If the Funcs + * are single-valued, the Tuple has one element and can be used as an Expr. */ // @{ -Expr select(const Expr &condition, const FuncRef &true_value, const FuncRef &false_value); +Tuple select(const Expr &condition, const FuncRef &true_value, const FuncRef &false_value); template -inline Expr select(const Expr &c0, const FuncRef &v0, const Expr &c1, const FuncRef &v1, Args &&...args) { - return select(c0, v0, select(c1, v1, std::forward(args)...)); +inline Tuple select(const Expr &c0, const FuncRef &v0, const Expr &c1, const FuncRef &v1, Args &&...args) { + return select(c0, Tuple(v0), c1, Tuple(v1), std::forward(args)...); } // @} @@ -1340,6 +1339,27 @@ inline HALIDE_NO_USER_CODE_INLINE Expr print(Expr a, Args &&...args) { } //@} +/** Create a Tuple that prints out all of its values whenever it is + * evaluated, followed by everything else in the arguments list. The + * print is attached to the first element of the Tuple; the other + * elements are returned unchanged. */ +template +inline HALIDE_NO_USER_CODE_INLINE Tuple print(const Tuple &a, Args &&...args) { + std::vector collected_args = a.as_vector(); + Internal::collect_print_args(collected_args, std::forward(args)...); + Tuple result = a; + result[0] = print(collected_args); + return result; +} + +/** print applied to a FuncRef prints all of the values of the Func, + * and returns a Tuple. If the Func is single-valued, the Tuple has + * one element and can be used as an Expr. */ +template +inline HALIDE_NO_USER_CODE_INLINE Tuple print(const FuncRef &a, Args &&...args) { + return print(Tuple(a), std::forward(args)...); +} + /** Create an Expr that prints whenever it is evaluated, provided that * the condition is true. */ // @{ @@ -1351,9 +1371,29 @@ inline HALIDE_NO_USER_CODE_INLINE Expr print_when(Expr condition, Expr a, Args & Internal::collect_print_args(collected_args, std::forward(args)...); return print_when(std::move(condition), collected_args); } - // @} +/** Create a Tuple that prints out all of its values whenever it is + * evaluated, provided that the condition is true. The print is + * attached to the first element of the Tuple; the other elements are + * returned unchanged. */ +template +inline HALIDE_NO_USER_CODE_INLINE Tuple print_when(Expr condition, const Tuple &a, Args &&...args) { + std::vector collected_args = a.as_vector(); + Internal::collect_print_args(collected_args, std::forward(args)...); + Tuple result = a; + result[0] = print_when(std::move(condition), collected_args); + return result; +} + +/** print_when applied to a FuncRef prints all of the values of the + * Func, and returns a Tuple. If the Func is single-valued, the Tuple + * has one element and can be used as an Expr. */ +template +inline HALIDE_NO_USER_CODE_INLINE Tuple print_when(Expr condition, const FuncRef &a, Args &&...args) { + return print_when(std::move(condition), Tuple(a), std::forward(args)...); +} + /** Create an Expr that that guarantees a precondition. * If 'condition' is true, the return value is equal to the first Expr. * If 'condition' is false, halide_error() is called, and the return value @@ -1386,6 +1426,29 @@ inline HALIDE_NO_USER_CODE_INLINE Expr require(Expr condition, Expr value, Args } // @} +/** Create a Tuple that guarantees a precondition. Each element of the + * result is the corresponding element of 'value' guarded by + * 'condition', as if by require. */ +template +inline HALIDE_NO_USER_CODE_INLINE Tuple require(const Expr &condition, const Tuple &value, Args &&...args) { + std::vector collected_args = {Expr()}; + Internal::collect_print_args(collected_args, std::forward(args)...); + Tuple result = value; + for (Expr &e : result) { + collected_args[0] = e; + e = require(condition, collected_args); + } + return result; +} + +/** require applied to a FuncRef guards all of the values of the Func, + * and returns a Tuple. If the Func is single-valued, the Tuple has + * one element and can be used as an Expr. */ +template +inline HALIDE_NO_USER_CODE_INLINE Tuple require(const Expr &condition, const FuncRef &value, Args &&...args) { + return require(condition, Tuple(value), std::forward(args)...); +} + /** Return an undef value of the given type. Halide skips stores that * depend on undef values, so you can use this to mean "do not modify * this memory location". This is an escape hatch that can be used for @@ -1460,6 +1523,26 @@ inline HALIDE_NO_USER_CODE_INLINE Expr memoize_tag(Expr result, Args &&...args) } // @} +/** Tag each element of a Tuple with the same cache key values, as if + * by memoize_tag. */ +template +inline HALIDE_NO_USER_CODE_INLINE Tuple memoize_tag(const Tuple &result, Args &&...args) { + std::vector collected_args{std::forward(args)...}; + Tuple tagged = result; + for (Expr &e : tagged) { + e = Internal::memoize_tag_helper(e, collected_args); + } + return tagged; +} + +/** memoize_tag applied to a FuncRef tags all of the values of the + * Func, and returns a Tuple. If the Func is single-valued, the Tuple + * has one element and can be used as an Expr. */ +template +inline HALIDE_NO_USER_CODE_INLINE Tuple memoize_tag(const FuncRef &result, Args &&...args) { + return memoize_tag(Tuple(result), std::forward(args)...); +} + /** Expressions tagged with this intrinsic are considered to be part * of the steady state of some loop with a nasty beginning and end * (e.g. a boundary condition). When Halide encounters likely @@ -1475,9 +1558,21 @@ inline HALIDE_NO_USER_CODE_INLINE Expr memoize_tag(Expr result, Args &&...args) */ Expr likely(Expr e); +/** Mark each element of a Tuple as likely. */ +Tuple likely(const Tuple &t); + +/** likely applied to a FuncRef marks all of the values of the Func as + * likely, and returns a Tuple. If the Func is single-valued, the + * Tuple has one element and can be used as an Expr. */ +Tuple likely(const FuncRef &f); + /** Equivalent to likely, but only triggers a loop partitioning if * found in an innermost loop. */ +// @{ Expr likely_if_innermost(Expr e); +Tuple likely_if_innermost(const Tuple &t); +Tuple likely_if_innermost(const FuncRef &f); +// @} /** Cast an expression to the halide type corresponding to the C++ * type T. As part of the cast, clamp to the minimum and maximum @@ -1496,7 +1591,11 @@ Expr saturating_cast(Type t, Expr e); * all backends. (E.g. it is difficult to do this for C++ code * generation as it depends on the compiler flags used to compile the * generated code. */ +// @{ Expr strict_float(const Expr &e); +Tuple strict_float(const Tuple &t); +Tuple strict_float(const FuncRef &f); +// @} /** Create an Expr that that promises another Expr is clamped but do * not generate code to check the assertion or modify the value. No diff --git a/src/IRPrinter.cpp b/src/IRPrinter.cpp index b4ca72d54e3a..0034db4fe25b 100644 --- a/src/IRPrinter.cpp +++ b/src/IRPrinter.cpp @@ -68,8 +68,8 @@ ostream &operator<<(ostream &stream, const Expr &ir) { ostream &operator<<(ostream &stream, const Tuple &ir) { stream << "("; - for (size_t i = 0; i < ir.size(); i++) { - stream << ir[i] << ", "; // keep the trailing comma + for (const Expr &e : ir) { + stream << e << ", "; // keep the trailing comma } return stream << ")"; } diff --git a/src/Tuple.cpp b/src/Tuple.cpp index cd39c9b516c0..3fa8ae37d942 100644 --- a/src/Tuple.cpp +++ b/src/Tuple.cpp @@ -11,12 +11,21 @@ Tuple::Tuple(const FuncRef &f) << "Can't call Func \"" << f.function().name() << "\" because it has not yet been defined.\n"; - user_assert(f.size() > 1) - << "Can't construct a Tuple from a call to Func \"" - << f.function().name() << "\" because it does not return a Tuple.\n"; - for (size_t i = 0; i < f.size(); i++) { - exprs[i] = f[i]; + if (f.size() == 1) { + exprs[0] = f; + } else { + for (size_t i = 0; i < f.size(); i++) { + exprs[i] = f[i]; + } } } +Tuple::operator Expr() const { + user_assert(exprs.size() == 1) + << "Can't treat this Tuple of size " << exprs.size() << " as an Expr:\n" + << (*this) << "\n" + << "Only one-element Tuples can be cast to Expr.\n"; + return exprs[0]; +} + } // namespace Halide diff --git a/src/Tuple.h b/src/Tuple.h index 7f775c578d14..04b9f96a0b8c 100644 --- a/src/Tuple.h +++ b/src/Tuple.h @@ -56,13 +56,33 @@ class Tuple { user_assert(!e.empty()) << "Tuples must have at least one element\n"; } - /** Construct a Tuple from a function reference. */ + /** Construct a Tuple from a function reference. A call to a + * single-valued Func makes a one-element Tuple. */ Tuple(const FuncRef &); + /** One-element Tuples can be used as Exprs directly. */ + operator Expr() const; + /** Treat the tuple as a vector of Exprs */ const std::vector &as_vector() const { return exprs; } + + /** Iterate over the elements. */ + // @{ + std::vector::iterator begin() { + return exprs.begin(); + } + std::vector::iterator end() { + return exprs.end(); + } + std::vector::const_iterator begin() const { + return exprs.begin(); + } + std::vector::const_iterator end() const { + return exprs.end(); + } + // @} }; } // namespace Halide diff --git a/test/correctness/CMakeLists.txt b/test/correctness/CMakeLists.txt index d88c13fa177f..b2414a2d20a0 100644 --- a/test/correctness/CMakeLists.txt +++ b/test/correctness/CMakeLists.txt @@ -373,6 +373,7 @@ tests( transpose_idioms.cpp transposed_vector_reduce.cpp trim_no_ops.cpp + tuple_helpers.cpp tuple_partial_update.cpp tuple_reduction.cpp tuple_select.cpp diff --git a/test/correctness/tuple_helpers.cpp b/test/correctness/tuple_helpers.cpp new file mode 100644 index 000000000000..51896af15ed2 --- /dev/null +++ b/test/correctness/tuple_helpers.cpp @@ -0,0 +1,299 @@ +#include "Halide.h" +#include +#include +#include + +using namespace Halide; +using namespace Halide::Internal; + +namespace { + +std::vector messages; + +void my_print(JITUserContext *user_context, const char *message) { + messages.push_back(message); +} + +bool error_occurred = false; + +void my_error(JITUserContext *user_context, const char *message) { + error_occurred = true; +} + +// Counts the For loops in the lowered Stmt. +class CountLoops : public IRMutator { + using IRMutator::visit; + + Stmt visit(const For *op) override { + count++; + return IRMutator::visit(op); + } + +public: + int count = 0; +}; + +// Check that each element of 'wrapped' is a call to the intrinsic +// 'op' whose first argument is the corresponding element of 'orig'. +bool check_wrapped(const Tuple &wrapped, const Tuple &orig, Call::IntrinsicOp op) { + if (wrapped.size() != orig.size()) { + printf("Tuple size changed from %d to %d\n", (int)orig.size(), (int)wrapped.size()); + return false; + } + for (size_t i = 0; i < wrapped.size(); i++) { + const Call *c = wrapped[i].as(); + if (!c || !c->is_intrinsic(op) || !equal(c->args[0], orig[i])) { + std::cerr << "Element " << i << " was not wrapped as expected: " << wrapped[i] << "\n"; + return false; + } + } + return true; +} + +} // namespace + +int main(int argc, char **argv) { + Target target = get_jit_target_from_environment(); + if (target.has_feature(Target::Profile) || target.has_feature(Target::Debug)) { + // Both add extra prints, so counting the number of prints is + // not useful. + printf("[SKIP] Test incompatible with profiler and debug runtime.\n"); + return 0; + } + + Var x("x"), y("y"); + Param p("p"), q("q"); + + // likely + { + Tuple t(x, y * 2); + if (!check_wrapped(likely(t), t, Call::likely)) { + return 1; + } + + // The likely should trigger loop partitioning, so there + // should be several loops over x. + Func f("f"); + f(x) = select(x < 10, Tuple(0, 0), likely(Tuple(x, x + 1))); + CountLoops counter; + f.add_custom_lowering_pass(&counter, []() {}); + Realization result = f.realize({20}); + if (counter.count < 2) { + printf("likely on a Tuple did not trigger loop partitioning\n"); + return 1; + } + Buffer a = result[0], b = result[1]; + for (int i = 0; i < 20; i++) { + int correct_a = i < 10 ? 0 : i; + int correct_b = i < 10 ? 0 : i + 1; + if (a(i) != correct_a || b(i) != correct_b) { + printf("result(%d) = (%d, %d) instead of (%d, %d)\n", + i, a(i), b(i), correct_a, correct_b); + return 1; + } + } + } + + // likely_if_innermost + { + Tuple t(x, y * 2); + if (!check_wrapped(likely_if_innermost(t), t, Call::likely_if_innermost)) { + return 1; + } + } + + // strict_float + { + Expr a = cast(x), b = cast(y); + Tuple t(a + b, a * b, x + y); + Tuple s = strict_float(t); + for (size_t i = 0; i < 2; i++) { + const Call *c = s[i].as(); + if (!c || !c->is_strict_float_intrinsic()) { + std::cerr << "strict_float did not strictify element " << i << ": " << s[i] << "\n"; + return 1; + } + } + if (!equal(s[2], t[2])) { + std::cerr << "strict_float changed an integer element: " << s[2] << "\n"; + return 1; + } + } + + // memoize_tag + { + Tuple t(x, y * 2); + Tuple m = memoize_tag(t, p, q); + if (!check_wrapped(m, t, Call::memoize_expr)) { + return 1; + } + for (size_t i = 0; i < m.size(); i++) { + const Call *c = m[i].as(); + if (c->args.size() != 3 || !equal(c->args[1], p) || !equal(c->args[2], q)) { + std::cerr << "memoize_tag did not attach the cache key values: " << m[i] << "\n"; + return 1; + } + } + } + + // require + { + Tuple t(x, y * 2); + Tuple r = require(p > 0, t, "p was", p); + // The value is the second argument of the require intrinsic. + for (size_t i = 0; i < r.size(); i++) { + const Call *c = r[i].as(); + if (!c || !c->is_intrinsic(Call::require) || !equal(c->args[1], t[i])) { + std::cerr << "require did not guard element " << i << ": " << r[i] << "\n"; + return 1; + } + } + + Func f("f"); + f(x) = require(p > 0, Tuple(x, x * 2), "p was", p); + f.jit_handlers().custom_error = my_error; + + p.set(1); + error_occurred = false; + Realization result = f.realize({10}); + if (error_occurred) { + printf("There should not have been a requirement error\n"); + return 1; + } + Buffer a = result[0], b = result[1]; + for (int i = 0; i < 10; i++) { + if (a(i) != i || b(i) != i * 2) { + printf("result(%d) = (%d, %d) instead of (%d, %d)\n", + i, a(i), b(i), i, i * 2); + return 1; + } + } + + p.set(0); + error_occurred = false; + f.realize(result); + if (!error_occurred) { + printf("There should have been a requirement error\n"); + return 1; + } + } + + // print + { + Tuple t(x, x * 2); + Tuple pr = print(t, "at", x); + if (!equal(pr[1], t[1])) { + std::cerr << "print changed the second element: " << pr[1] << "\n"; + return 1; + } + + Func f("f"); + f(x) = print(Tuple(x, x * 2), "at", x); + f.jit_handlers().custom_print = my_print; + messages.clear(); + Realization result = f.realize({3}); + Buffer a = result[0], b = result[1]; + for (int i = 0; i < 3; i++) { + if (a(i) != i || b(i) != i * 2) { + printf("result(%d) = (%d, %d) instead of (%d, %d)\n", + i, a(i), b(i), i, i * 2); + return 1; + } + } + std::vector expected = {"0 0 at 0\n", "1 2 at 1\n", "2 4 at 2\n"}; + if (messages != expected) { + printf("print on a Tuple printed the wrong thing:\n"); + for (const auto &m : messages) { + printf(" %s", m.c_str()); + } + return 1; + } + } + + // print_when + { + Func f("f"); + f(x) = print_when(x == 1, Tuple(x, x * 2), "at", x); + f.jit_handlers().custom_print = my_print; + messages.clear(); + Realization result = f.realize({3}); + Buffer a = result[0], b = result[1]; + for (int i = 0; i < 3; i++) { + if (a(i) != i || b(i) != i * 2) { + printf("result(%d) = (%d, %d) instead of (%d, %d)\n", + i, a(i), b(i), i, i * 2); + return 1; + } + } + std::vector expected = {"1 2 at 1\n"}; + if (messages != expected) { + printf("print_when on a Tuple printed the wrong thing:\n"); + for (const auto &m : messages) { + printf(" %s", m.c_str()); + } + return 1; + } + } + + // FuncRefs convert to both Expr and Tuple, so they get their own + // overloads, which return Tuples. A one-element Tuple can be used + // as an Expr, so single-valued Funcs work in Expr contexts. + { + Func g("g"), h("h"); + g(x) = x; + h(x) = Tuple(x, x * 2); + + Expr e = likely(g(x)); + const Call *c = e.as(); + if (!c || !c->is_intrinsic(Call::likely) || !equal(c->args[0], Expr(g(x)))) { + std::cerr << "likely on a FuncRef gave " << e << "\n"; + return 1; + } + + Tuple t = likely(h(x)); + if (!check_wrapped(t, Tuple(h(x)), Call::likely)) { + return 1; + } + + // The other helpers should accept FuncRefs without ambiguity, + // whether or not the Func is single-valued. + e = likely_if_innermost(g(x)); + e = strict_float(g(x)); + e = memoize_tag(g(x), p); + e = require(p > 0, g(x), "p was", p); + e = print(g(x), "at", x); + e = print_when(x == 1, g(x), "at", x); + e = select(x < 10, g(x), g(x + 1)); + e = likely(g(x)) + 1; + t = likely_if_innermost(h(x)); + t = strict_float(h(x)); + t = memoize_tag(h(x), p); + t = require(p > 0, h(x), "p was", p); + t = print(h(x), "at", x); + t = print_when(x == 1, h(x), "at", x); + t = select(x < 10, h(x), h(x + 1)); + } + + // Tuples can be iterated over. + { + Tuple t(x, y * 2); + for (Expr &e : t) { + e = likely(e); + } + if (!check_wrapped(t, Tuple(x, y * 2), Call::likely)) { + return 1; + } + } + + // One-element Tuples convert to Exprs. + { + Expr e = Tuple(x + 1); + if (!equal(e, x + 1)) { + std::cerr << "Converting a one-element Tuple to an Expr gave " << e << "\n"; + return 1; + } + } + + printf("Success!\n"); + return 0; +} diff --git a/test/error/CMakeLists.txt b/test/error/CMakeLists.txt index 5b949b48a112..0f6c8dbdc339 100644 --- a/test/error/CMakeLists.txt +++ b/test/error/CMakeLists.txt @@ -141,6 +141,7 @@ tests( tuple_arg_select_undef.cpp tuple_output_bounds_check.cpp tuple_realization_to_buffer.cpp + tuple_to_expr.cpp tuple_val_select_undef.cpp unbounded_input.cpp unbounded_output.cpp diff --git a/test/error/tuple_to_expr.cpp b/test/error/tuple_to_expr.cpp new file mode 100644 index 000000000000..cf864bdd578e --- /dev/null +++ b/test/error/tuple_to_expr.cpp @@ -0,0 +1,14 @@ +#include "Halide.h" +#include + +using namespace Halide; + +int main(int argc, char **argv) { + Var x("x"), y("y"); + + // Only one-element Tuples can be used as Exprs. + Expr e = Tuple(x, y); + + printf("Success!\n"); + return 0; +}