diff --git a/src/Simplify_Add.cpp b/src/Simplify_Add.cpp index 6158cc9cd48c..440a154ec521 100644 --- a/src/Simplify_Add.cpp +++ b/src/Simplify_Add.cpp @@ -25,8 +25,16 @@ Expr Simplify::visit(const Add *op, ExprInfo *info) { if (rewrite(IRMatcher::Overflow() + x, a) || rewrite(x + IRMatcher::Overflow(), b) || - rewrite(x + 0, x) || - rewrite(0 + x, x)) { + rewrite(x + 0, a) || + rewrite(0 + x, b)) { + if (info) { + if (rewrite.result.same_as(a)) { + info->intersect(a_info); + } else { + internal_assert(rewrite.result.same_as(b)); + info->intersect(b_info); + } + } return rewrite.result; } diff --git a/src/Simplify_Call.cpp b/src/Simplify_Call.cpp index 593d05530cbd..a49894b4e47f 100644 --- a/src/Simplify_Call.cpp +++ b/src/Simplify_Call.cpp @@ -282,10 +282,11 @@ Expr Simplify::visit(const Call *op, ExprInfo *info) { // pattern minus x. We get more information that way than just // counting the leading zeros or ones. Expr e = mutate(make_const(op->type, (int64_t)(-1), nullptr) - a, info); - // If the result of this happens to be a constant, we may as well - // return it. This is redundant with the constant folding below, but - // the constant folding below still needs to happen when info is - // nullptr. + // If the result of this happens to fold to a constant, we may as + // well return it immediately. This only happens if a is a constant + // uint or int, in which case the logic below would produce the + // exact same constant Expr with the exact same info as we're + // already holding. if (info->bounds.is_single_point()) { return e; } diff --git a/src/Simplify_Cast.cpp b/src/Simplify_Cast.cpp index 8738c26aeb97..25d790d087a9 100644 --- a/src/Simplify_Cast.cpp +++ b/src/Simplify_Cast.cpp @@ -17,7 +17,13 @@ Expr Simplify::visit(const Cast *op, ExprInfo *info) { // know. *info = ExprInfo{}; } else { + int64_t old_min = value_info.bounds.min; value_info.cast_to(op->type); + if (op->type.is_uint() && op->type.bits() == 64 && old_min > 0) { + // It's impossible for a cast *to* a uint64 in Halide to lower the + // min. Casts to uint64_t don't overflow for any source type. + value_info.bounds.min = old_min; + } value_info.trim_bounds_using_alignment(); if (info) { *info = value_info; @@ -25,7 +31,7 @@ Expr Simplify::visit(const Cast *op, ExprInfo *info) { // It's possible we just reduced to a constant. E.g. if we cast an // even number to uint1 we get zero. if (value_info.bounds.is_single_point()) { - return make_const(op->type, value_info.bounds.min, nullptr); + return make_const(op->type, value_info.bounds.min, info); } } diff --git a/src/Simplify_Div.cpp b/src/Simplify_Div.cpp index cf3d796e2b67..159e7bab802f 100644 --- a/src/Simplify_Div.cpp +++ b/src/Simplify_Div.cpp @@ -8,40 +8,44 @@ Expr Simplify::visit(const Div *op, ExprInfo *info) { Expr a = mutate(op->a, &a_info); Expr b = mutate(op->b, &b_info); - if (info) { - if (op->type.is_int_or_uint()) { - // ConstantInterval division is integer division, so we can't use - // this code path for floats. - info->bounds = a_info.bounds / b_info.bounds; - info->alignment = a_info.alignment / b_info.alignment; - info->cast_to(op->type); - info->trim_bounds_using_alignment(); - - // Bounded numerator divided by constantish bounded denominator can - // sometimes collapse things to a constant at this point. This - // mostly happens when the denominator is a constant and the - // numerator span is small (e.g. [23, 29]/10 = 2), but there are - // also cases with a bounded denominator (e.g. [5, 7]/[4, 5] = 1). - if (info->bounds.is_single_point()) { - if (op->type.can_represent(info->bounds.min)) { - return make_const(op->type, info->bounds.min, nullptr); - } else { - // Even though this is 'no-overflow-int', if the result - // we calculate can't fit into the destination type, - // we're better off returning an overflow condition than - // a known-wrong value. (Note that no_overflow_int() should - // only be true for signed integers.) - internal_assert(no_overflow_int(op->type)) << op->type << " " << info->bounds; - clear_expr_info(info); - return make_signed_integer_overflow(op->type); - } + ExprInfo div_info; + + if (op->type.is_int_or_uint()) { + // ConstantInterval division is integer division, so we can't use + // this code path for floats. + div_info.bounds = a_info.bounds / b_info.bounds; + div_info.alignment = a_info.alignment / b_info.alignment; + div_info.cast_to(op->type); + div_info.trim_bounds_using_alignment(); + + // Bounded numerator divided by constantish bounded denominator can + // sometimes collapse things to a constant at this point. This + // mostly happens when the denominator is a constant and the + // numerator span is small (e.g. [23, 29]/10 = 2), but there are + // also cases with a bounded denominator (e.g. [5, 7]/[4, 5] = 1). + if (div_info.bounds.is_single_point()) { + if (op->type.can_represent(div_info.bounds.min)) { + return make_const(op->type, div_info.bounds.min, info); + } else { + // Even though this is 'no-overflow-int', if the result + // we calculate can't fit into the destination type, + // we're better off returning an overflow condition than + // a known-wrong value. (Note that no_overflow_int() should + // only be true for signed integers.) + internal_assert(no_overflow_int(op->type)) << op->type << " " << div_info.bounds; + clear_expr_info(info); + return make_signed_integer_overflow(op->type); } - } else { - // TODO: Tracking constant integer bounds of floating point values - // isn't so useful right now, but if we want integer bounds for - // floating point division later, here's the place to put it. - clear_expr_info(info); } + } else { + // TODO: Tracking constant integer bounds of floating point values isn't + // so useful right now, but if we want integer bounds for floating point + // division later, here's the place to put it. Just leave div_info empty + // for now (i.e. nothing is known). + } + + if (info) { + *info = div_info; } bool denominator_non_zero = @@ -55,9 +59,17 @@ Expr Simplify::visit(const Div *op, ExprInfo *info) { if (rewrite(IRMatcher::Overflow() / x, a) || rewrite(x / IRMatcher::Overflow(), b) || - rewrite(x / 1, x) || - rewrite(0 / x, 0) || + rewrite(x / 1, a) || + rewrite(0 / x, a) || false) { + if (info) { + if (rewrite.result.same_as(a)) { + info->intersect(a_info); + } else { + internal_assert(rewrite.result.same_as(b)); + info->intersect(b_info); + } + } return rewrite.result; } diff --git a/src/Simplify_Exprs.cpp b/src/Simplify_Exprs.cpp index bbd67a5bace0..8fb7554ec84b 100644 --- a/src/Simplify_Exprs.cpp +++ b/src/Simplify_Exprs.cpp @@ -19,11 +19,17 @@ Expr Simplify::visit(const IntImm *op, ExprInfo *info) { } Expr Simplify::visit(const UIntImm *op, ExprInfo *info) { - if (info && Int(64).can_represent(op->value)) { + if (info) { + // Pretend it's an int constant that has been cast to uint. int64_t v = (int64_t)(op->value); info->bounds = ConstantInterval::single_point(v); info->alignment = ModulusRemainder(0, v); + // If it's not representable as an int64, this will wrap the alignment appropriately: info->cast_to(op->type); + // Be as informative as we can with bounds for out-of-range uint64s + if ((int64_t)op->value < 0) { + info->bounds = ConstantInterval::bounded_below(INT64_MAX); + } } else { clear_expr_info(info); } diff --git a/src/Simplify_Internal.h b/src/Simplify_Internal.h index 7c42f1ece309..ea1c1e80f386 100644 --- a/src/Simplify_Internal.h +++ b/src/Simplify_Internal.h @@ -124,7 +124,15 @@ class Simplify : public VariadicVisitor { } } - // Truncate the bounds to the new type. + // For UInt64 constants, the remainder might not be representable as an int64 + if (t.bits() == 64 && t.is_uint() && + alignment.modulus == 0 && alignment.remainder < 0) { + // Forget the leading two bits to get a representable modulus + // and remainder. + alignment.modulus = (int64_t)1 << 62; + alignment.remainder = alignment.remainder & (alignment.modulus - 1); + } + bounds.cast_to(t); } @@ -241,6 +249,10 @@ class Simplify : public VariadicVisitor { // We never want to return make_const anything in the simplifier without // also setting the ExprInfo, so shadow the global make_const. Expr make_const(const Type &t, int64_t c, ExprInfo *info) { + if (t.is_uint() && c < 0) { + // Wrap it around + return make_const(t, (uint64_t)c, info); + } c = normalize_constant(t, c); set_expr_info_to_constant(info, c); return Halide::Internal::make_const(t, c); diff --git a/src/Simplify_Max.cpp b/src/Simplify_Max.cpp index 1926bc9a069e..db5d676af427 100644 --- a/src/Simplify_Max.cpp +++ b/src/Simplify_Max.cpp @@ -21,8 +21,10 @@ Expr Simplify::visit(const Max *op, ExprInfo *info) { if (max_info.bounds.is_single_point()) { // This is possible when, for example, the largest number in the type // that satisfies the alignment of the left-hand-side is smaller than - // the min value of the right-hand-side. - return make_const(op->type, max_info.bounds.min, nullptr); + // the min value of the right-hand-side. Reinferring the info can + // potentially give us something tighter than what was computed above if + // it's a large uint64. + return make_const(op->type, max_info.bounds.min, info); } auto strip_likely = [](const Expr &e) { @@ -65,10 +67,10 @@ Expr Simplify::visit(const Max *op, ExprInfo *info) { return rewrite.result; } + // Cases where one side dominates. All of these must reduce to a or b in the + // RHS for ExprInfo to update correctly. if (EVAL_IN_LAMBDA // (rewrite(max(x, x), a) || - rewrite(max(c0, c1), fold(max(c0, c1))) || - // Cases where one side dominates: rewrite(max(x, c0), b, is_max_value(c0)) || rewrite(max(x, c0), a, is_min_value(c0)) || rewrite(max((x / c0) * c0, x), b, c0 > 0) || @@ -148,16 +150,17 @@ Expr Simplify::visit(const Max *op, ExprInfo *info) { // than just applying max to two constant intervals. if (rewrite.result.same_as(a)) { info->intersect(a_info); - } else if (rewrite.result.same_as(b)) { + } else { + internal_assert(rewrite.result.same_as(b)); info->intersect(b_info); } } - return rewrite.result; } if (EVAL_IN_LAMBDA // - (rewrite(max(max(x, c0), c1), max(x, fold(max(c0, c1)))) || + (rewrite(max(c0, c1), fold(max(c0, c1))) || + rewrite(max(max(x, c0), c1), max(x, fold(max(c0, c1)))) || rewrite(max(max(x, c0), y), max(max(x, y), c0)) || rewrite(max(max(x, y), max(x, z)), max(max(y, z), x)) || rewrite(max(max(y, x), max(x, z)), max(max(y, z), x)) || diff --git a/src/Simplify_Min.cpp b/src/Simplify_Min.cpp index 3f6084c6c4f1..9c44a950818d 100644 --- a/src/Simplify_Min.cpp +++ b/src/Simplify_Min.cpp @@ -22,7 +22,7 @@ Expr Simplify::visit(const Min *op, ExprInfo *info) { // This is possible when, for example, the smallest number in the type // that satisfies the alignment of the left-hand-side is greater than // the max value of the right-hand-side. - return make_const(op->type, min_info.bounds.min, nullptr); + return make_const(op->type, min_info.bounds.min, info); } // Early out when the bounds tells us one side or the other is smaller @@ -66,10 +66,10 @@ Expr Simplify::visit(const Min *op, ExprInfo *info) { return rewrite.result; } + // Cases where one side dominates. All of these must reduce to a or b in the + // RHS for ExprInfo to update correctly. if (EVAL_IN_LAMBDA // (rewrite(min(x, x), a) || - rewrite(min(c0, c1), fold(min(c0, c1))) || - // Cases where one side dominates: rewrite(min(x, c0), b, is_min_value(c0)) || rewrite(min(x, c0), a, is_max_value(c0)) || rewrite(min((x / c0) * c0, x), a, c0 > 0) || @@ -148,7 +148,8 @@ Expr Simplify::visit(const Min *op, ExprInfo *info) { if (info) { if (rewrite.result.same_as(a)) { info->intersect(a_info); - } else if (rewrite.result.same_as(b)) { + } else { + internal_assert(rewrite.result.same_as(b)); info->intersect(b_info); } } @@ -156,7 +157,8 @@ Expr Simplify::visit(const Min *op, ExprInfo *info) { } if (EVAL_IN_LAMBDA // - (rewrite(min(min(x, c0), c1), min(x, fold(min(c0, c1)))) || + (rewrite(min(c0, c1), fold(min(c0, c1))) || + rewrite(min(min(x, c0), c1), min(x, fold(min(c0, c1)))) || rewrite(min(min(x, c0), y), min(min(x, y), c0)) || rewrite(min(min(x, y), min(x, z)), min(min(y, z), x)) || rewrite(min(min(y, x), min(x, z)), min(min(y, z), x)) || diff --git a/src/Simplify_Mul.cpp b/src/Simplify_Mul.cpp index dfa38d39111c..58032ab79d62 100644 --- a/src/Simplify_Mul.cpp +++ b/src/Simplify_Mul.cpp @@ -41,10 +41,18 @@ Expr Simplify::visit(const Mul *op, ExprInfo *info) { return rewrite.result; } - if (rewrite(0 * x, 0) || - rewrite(1 * x, x) || - rewrite(x * 0, 0) || - rewrite(x * 1, x)) { + if (rewrite(0 * x, a) || + rewrite(1 * x, b) || + rewrite(x * 0, b) || + rewrite(x * 1, a)) { + if (info) { + if (rewrite.result.same_as(a)) { + info->intersect(a_info); + } else { + internal_assert(rewrite.result.same_as(b)); + info->intersect(b_info); + } + } return rewrite.result; } diff --git a/src/Simplify_Select.cpp b/src/Simplify_Select.cpp index 3bc4507fc74b..e78d02014eda 100644 --- a/src/Simplify_Select.cpp +++ b/src/Simplify_Select.cpp @@ -34,7 +34,8 @@ Expr Simplify::visit(const Select *op, ExprInfo *info) { if (info) { if (rewrite.result.same_as(true_value)) { *info = t_info; - } else if (rewrite.result.same_as(false_value)) { + } else { + internal_assert(rewrite.result.same_as(false_value)); *info = f_info; } } diff --git a/src/Simplify_Sub.cpp b/src/Simplify_Sub.cpp index 29bd02c78ed6..a6fcc9e675ce 100644 --- a/src/Simplify_Sub.cpp +++ b/src/Simplify_Sub.cpp @@ -22,7 +22,15 @@ Expr Simplify::visit(const Sub *op, ExprInfo *info) { if (rewrite(IRMatcher::Overflow() - x, a) || rewrite(x - IRMatcher::Overflow(), b) || - rewrite(x - 0, x)) { + rewrite(x - 0, a)) { + if (info) { + if (rewrite.result.same_as(a)) { + info->intersect(a_info); + } else { + internal_assert(rewrite.result.same_as(b)); + info->intersect(b_info); + } + } return rewrite.result; } diff --git a/test/correctness/CMakeLists.txt b/test/correctness/CMakeLists.txt index 6d41a9e71219..e1c44561be00 100644 --- a/test/correctness/CMakeLists.txt +++ b/test/correctness/CMakeLists.txt @@ -126,7 +126,6 @@ tests(GROUPS correctness fused_where_inner_extent_is_zero.cpp fuzz_float_stores.cpp fuzz_schedule.cpp - fuzz_simplify.cpp gameoflife.cpp gather.cpp gpu_alloc_group_profiling.cpp @@ -356,7 +355,6 @@ tests(GROUPS correctness vectorized_initialization.cpp vectorized_load_from_vectorized_allocation.cpp vectorized_reduction_bug.cpp - widening_lerp.cpp widening_reduction.cpp # keep-sorted end ) diff --git a/test/correctness/fuzz_simplify.cpp b/test/correctness/fuzz_simplify.cpp deleted file mode 100644 index 52f9354d0f35..000000000000 --- a/test/correctness/fuzz_simplify.cpp +++ /dev/null @@ -1,197 +0,0 @@ -#include "Halide.h" -#include -#include -#include - -#include "random_expr_generator.h" - -// Test the simplifier in Halide by testing for equivalence of randomly generated expressions. -namespace { - -using std::map; -using std::string; -using namespace Halide; -using namespace Halide::Internal; - -using RandomEngine = RandomExpressionGenerator::RandomEngine; - -bool test_simplification(Expr a, Expr b, Type t, const map &vars) { - if (equal(a, b) && !a.same_as(b)) { - std::cerr << "Simplifier created new IR node but made no changes:\n" - << a << "\n"; - return false; - } - if (Expr sb = simplify(b); !equal(b, sb)) { - std::cerr << "Idempotency failure!\n " << a << "\n -> " << b << "\n -> " << sb << "\n"; - // These are broken out below to make it easier to parse any logging - // added to the simplifier to debug the failure. - std::cerr << "---------------------------------\n" - << "Begin simplification of original:\n" - << simplify(a) << "\n"; - std::cerr << "---------------------------------\n" - << "Begin resimplification of result:\n" - << simplify(b) << "\n" - << "---------------------------------\n"; - - return false; - } - - Expr a_v = simplify(substitute(vars, a)); - Expr b_v = simplify(substitute(vars, b)); - // If the simplifier didn't produce constants, there must be - // undefined behavior in this expression. Ignore it. - if (!Internal::is_const(a_v) || !Internal::is_const(b_v)) { - return true; - } - if (!equal(a_v, b_v)) { - std::cerr << "Simplified Expr is not equal() to Original Expr!\n"; - - for (const auto &[var, val] : vars) { - std::cerr << "Var " << var << " = " << val << "\n"; - } - - std::cerr << "Original Expr is: " << a << "\n"; - std::cerr << "Simplified Expr is: " << b << "\n"; - std::cerr << " " << a << " -> " << a_v << "\n"; - std::cerr << " " << b << " -> " << b_v << "\n"; - return false; - } - - return true; -} - -bool test_expression(RandomExpressionGenerator ®, RandomEngine &rng, Expr test, int samples) { - Expr simplified = simplify(test); - - map vars; - for (int i = 0; i < (int)reg.fuzz_vars.size(); i++) { - vars[reg.fuzz_var(i)] = Expr(); - } - - for (int i = 0; i < samples; i++) { - for (auto &[var, val] : vars) { - constexpr size_t kMaxLeafIterations = 10000; - // Don't let the random leaf depend on v itself. - size_t iterations = 0; - do { - val = reg.random_leaf(rng, Int(32), true); - iterations++; - } while (expr_uses_var(val, var) && iterations < kMaxLeafIterations); - } - - if (!test_simplification(test, simplified, test.type(), vars)) { - return false; - } - } - return true; -} - -template -T initialize_rng() { - constexpr size_t kStateWords = T::state_size * sizeof(typename T::result_type) / sizeof(uint32_t); - std::vector random(kStateWords); - std::generate(random.begin(), random.end(), std::random_device{}); - std::seed_seq seed_seq(random.begin(), random.end()); - return T{seed_seq}; -} - -} // namespace - -int main(int argc, char **argv) { - // Depth of the randomly generated expression trees. - constexpr int depth = 6; - // Number of samples to test the generated expressions for. - constexpr int samples = 3; - - auto seed_generator = initialize_rng(); - - RandomExpressionGenerator reg; - reg.fuzz_types = {UInt(1), UInt(8), UInt(16), UInt(32), Int(8), Int(16), Int(32)}; - // FIXME: UInt64 fails! - // FIXME: These need to be disabled (otherwise crashes and/or failures): - // reg.gen_ramp_of_vector = false; - // reg.gen_broadcast_of_vector = false; - reg.gen_vector_reduce = false; - reg.gen_reinterpret = false; - reg.gen_shuffles = false; - - for (int i = 0; i < ((argc == 1) ? 10000 : 1); i++) { - auto seed = seed_generator(); - if (argc > 1) { - std::istringstream{argv[1]} >> seed; - } - // Print the seed on every iteration so that if the simplifier crashes - // (rather than the check failing), we can reproduce. - std::cout << "Seed: " << seed << "\n"; - RandomEngine rng{seed}; - std::array vector_widths = {1, 2, 3, 4, 6, 8}; - int width = reg.random_choice(rng, vector_widths); - Type VT = reg.random_type(rng, width); - // Generate a random expr... - Expr test = reg.random_expr(rng, VT, depth); - std::cout << test << "\n"; - if (!test_expression(reg, rng, test, samples)) { - - class LimitDepth : public IRMutator { - int limit; - - public: - using IRMutator::mutate; - - Expr mutate(const Expr &e) override { - if (limit == 0) { - return simplify(e); - } else { - limit--; - Expr new_e = IRMutator::mutate(e); - limit++; - return new_e; - } - } - - LimitDepth(int l) - : limit(l) { - } - }; - - // Failure. Find the minimal subexpression that failed. - std::cout << "Testing subexpressions...\n"; - class TestSubexpressions : public IRMutator { - RandomExpressionGenerator reg; - RandomEngine &rng; - bool found_failure = false; - - public: - using IRMutator::mutate; - Expr mutate(const Expr &e) override { - // We know there's a failure here somewhere, so test - // subexpressions more aggressively. - constexpr int samples = 100; - IRMutator::mutate(e); - if (e.type().bits() && !found_failure) { - Expr limited; - for (int i = 1; i < 4 && !found_failure; i++) { - limited = LimitDepth(i).mutate(e); - found_failure = !test_expression(reg, rng, limited, samples); - } - if (!found_failure) { - found_failure = !test_expression(reg, rng, e, samples); - } - } - return e; - } - - TestSubexpressions(RandomExpressionGenerator ®, RandomEngine &rng) - : reg(reg), rng(rng) { - } - } tester(reg, rng); - tester.mutate(test); - - std::cout << "Failed with seed " << seed << "\n"; - return 1; - } - } - - std::cout << "Success!\n"; - return 0; -} diff --git a/test/correctness/lossless_cast.cpp b/test/correctness/lossless_cast.cpp index 7633954fb003..b3e055ce3fe3 100644 --- a/test/correctness/lossless_cast.cpp +++ b/test/correctness/lossless_cast.cpp @@ -3,7 +3,7 @@ using namespace Halide; using namespace Halide::Internal; -int check_lossless_cast(const Type &t, const Expr &in, const Expr &correct) { +bool check_lossless_cast(const Type &t, const Expr &in, const Expr &correct) { Expr result = lossless_cast(t, in); if (!equal(result, correct)) { std::cout << "Incorrect lossless_cast result:\n" @@ -11,12 +11,12 @@ int check_lossless_cast(const Type &t, const Expr &in, const Expr &correct) { << " " << result << " but expected was:\n" << " " << correct << "\n"; - return 1; + return true; } - return 0; + return false; } -int lossless_cast_test() { +int main(int argc, char **argv) { Expr x = Variable::make(Int(32), "x"); Type u8 = UInt(8); Type u16 = UInt(16); @@ -33,53 +33,53 @@ int lossless_cast_test() { Expr var_u16 = Variable::make(u16, "x"); Expr var_u8x = Variable::make(u8x, "x"); - int res = 0; + bool found_error = false; Expr e = cast(u8, x); - res |= check_lossless_cast(i32, e, cast(i32, e)); + found_error |= check_lossless_cast(i32, e, cast(i32, e)); e = cast(u8, x); - res |= check_lossless_cast(i32, e, cast(i32, e)); + found_error |= check_lossless_cast(i32, e, cast(i32, e)); e = cast(i8, var_u16); - res |= check_lossless_cast(u16, e, Expr()); + found_error |= check_lossless_cast(u16, e, Expr()); e = cast(i16, var_u16); - res |= check_lossless_cast(u16, e, Expr()); + found_error |= check_lossless_cast(u16, e, Expr()); e = cast(u32, var_u8); - res |= check_lossless_cast(u16, e, cast(u16, var_u8)); + found_error |= check_lossless_cast(u16, e, cast(u16, var_u8)); e = VectorReduce::make(VectorReduce::Add, cast(u16x, var_u8x), 1); - res |= check_lossless_cast(u16, e, cast(u16, e)); + found_error |= check_lossless_cast(u16, e, cast(u16, e)); e = VectorReduce::make(VectorReduce::Add, cast(u32x, var_u8x), 1); - res |= check_lossless_cast(u16, e, VectorReduce::make(VectorReduce::Add, cast(u16x, var_u8x), 1)); + found_error |= check_lossless_cast(u16, e, VectorReduce::make(VectorReduce::Add, cast(u16x, var_u8x), 1)); e = cast(u32, var_u8) - 16; - res |= check_lossless_cast(u16, e, Expr()); + found_error |= check_lossless_cast(u16, e, Expr()); e = cast(u32, var_u8) + 16; - res |= check_lossless_cast(u16, e, cast(u16, var_u8) + 16); + found_error |= check_lossless_cast(u16, e, cast(u16, var_u8) + 16); e = 16 - cast(u32, var_u8); - res |= check_lossless_cast(u16, e, Expr()); + found_error |= check_lossless_cast(u16, e, Expr()); e = 16 + cast(u32, var_u8); - res |= check_lossless_cast(u16, e, 16 + cast(u16, var_u8)); + found_error |= check_lossless_cast(u16, e, 16 + cast(u16, var_u8)); // Check one where the target type is unsigned but there's a signed addition // (that can't overflow) e = cast(i64, cast(u16, var_u8) + cast(i32, 17)); - res |= check_lossless_cast(u32, e, cast(u32, cast(u16, var_u8)) + cast(u32, 17)); + found_error |= check_lossless_cast(u32, e, cast(u32, cast(u16, var_u8)) + cast(u32, 17)); // Check one where the target type is unsigned but there's a signed subtract // (that can overflow). It's not safe to enter the i16 sub e = cast(i64, cast(i16, 10) - cast(i16, 17)); - res |= check_lossless_cast(u32, e, Expr()); + found_error |= check_lossless_cast(u32, e, Expr()); e = cast(i64, 1024) * cast(i64, 1024) * cast(i64, 1024); - res |= check_lossless_cast(i32, e, (cast(i32, 1024) * 1024) * 1024); + found_error |= check_lossless_cast(i32, e, (cast(i32, 1024) * 1024) * 1024); // Check narrowing a vector reduction of something narrowable to bool ... auto make_reduce = [&](Type t, VectorReduce::Operator op) { @@ -89,396 +89,24 @@ int lossless_cast_test() { // It's OK to narrow it to 8-bit. e = make_reduce(UInt(16), VectorReduce::Add); - res |= check_lossless_cast(UInt(8), e, make_reduce(UInt(8), VectorReduce::Add)); + found_error |= check_lossless_cast(UInt(8), e, make_reduce(UInt(8), VectorReduce::Add)); // ... but we can't reduce it all the way to bool if the operator isn't // legal for bools (issue #9011) e = make_reduce(UInt(8), VectorReduce::Add); - res |= check_lossless_cast(Bool(), e, Expr()); + found_error |= check_lossless_cast(Bool(), e, Expr()); // Min or Max, however, can just become And and Or e = make_reduce(UInt(8), VectorReduce::Min); - res |= check_lossless_cast(Bool(), e, make_reduce(Bool(), VectorReduce::And)); + found_error |= check_lossless_cast(Bool(), e, make_reduce(Bool(), VectorReduce::And)); e = make_reduce(UInt(8), VectorReduce::Max); - res |= check_lossless_cast(Bool(), e, make_reduce(Bool(), VectorReduce::Or)); - - return res; -} - -constexpr int size = 1024; -Buffer buf_u8(size, "buf_u8"); -Buffer buf_i8(size, "buf_i8"); -Var x{"x"}; - -Expr random_expr(std::mt19937 &rng) { - std::vector exprs; - // Add some atoms - exprs.push_back(cast((uint8_t)rng())); - exprs.push_back(cast((int8_t)rng())); - exprs.push_back(cast((uint8_t)rng())); - exprs.push_back(cast((int8_t)rng())); - exprs.push_back(buf_u8(x)); - exprs.push_back(buf_i8(x)); - - // Make random combinations of them - while (true) { - Expr e; - int i1 = rng() % exprs.size(); - int i2 = rng() % exprs.size(); - int i3 = rng() % exprs.size(); - int op = rng() % 8; - Expr e1 = exprs[i1]; - Expr e2 = cast(e1.type(), exprs[i2]); - Expr e3 = cast(e1.type().with_code(halide_type_uint), exprs[i3]); - bool may_widen = e1.type().bits() < 64; - Expr e2_narrow = exprs[i2]; - bool may_widen_right = e2_narrow.type() == e1.type().narrow(); - switch (op) { - case 0: - if (may_widen) { - e = cast(e1.type().widen(), e1); - } - break; - case 1: - if (may_widen) { - e = cast(Int(e1.type().bits() * 2), e1); - } - break; - case 2: - e = e1 + e2; - break; - case 3: - e = e1 - e2; - break; - case 4: - e = e1 * e2; - break; - case 5: - e = e1 / e2; - break; - case 6: - // Introduce some lets - e = common_subexpression_elimination(e1); - break; - case 7: - switch (rng() % 20) { - case 0: - if (may_widen) { - e = widening_add(e1, e2); - } - break; - case 1: - if (may_widen) { - e = widening_sub(e1, e2); - } - break; - case 2: - if (may_widen) { - e = widening_mul(e1, e2); - } - break; - case 3: - e = halving_add(e1, e2); - break; - case 4: - e = rounding_halving_add(e1, e2); - break; - case 5: - e = halving_sub(e1, e2); - break; - case 6: - e = saturating_add(e1, e2); - break; - case 7: - e = saturating_sub(e1, e2); - break; - case 8: - e = count_leading_zeros(e1); - break; - case 9: - e = count_trailing_zeros(e1); - break; - case 10: - if (may_widen) { - e = rounding_mul_shift_right(e1, e2, e3); - } - break; - case 11: - if (may_widen) { - e = mul_shift_right(e1, e2, e3); - } - break; - case 12: - if (may_widen_right) { - e = widen_right_add(e1, e2_narrow); - } - break; - case 13: - if (may_widen_right) { - e = widen_right_sub(e1, e2_narrow); - } - break; - case 14: - if (may_widen_right) { - e = widen_right_mul(e1, e2_narrow); - } - break; - case 15: - e = e1 << e2; - break; - case 16: - e = e1 >> e2; - break; - case 17: - e = rounding_shift_right(e1, e2); - break; - case 18: - e = rounding_shift_left(e1, e2); - break; - case 19: - e = ~e1; - break; - } - } - - if (!e.defined()) { - continue; - } - - // Stop when we get to 64 bits, but probably don't stop on a cast, - // because that'll just get trivially stripped. - if (e.type().bits() == 64 && (e.as() == nullptr || ((rng() & 7) == 0))) { - return e; - } - - exprs.push_back(e); - } -} - -bool definitely_has_ub(Expr e) { - e = simplify(e); - - class HasOverflow : public IRVisitor { - void visit(const Call *op) override { - if (op->is_intrinsic({Call::signed_integer_overflow})) { - found = true; - } - IRVisitor::visit(op); - } - - public: - bool found = false; - } has_overflow; - e.accept(&has_overflow); - - return has_overflow.found; -} - -bool might_have_ub(Expr e) { - class MightOverflow : public IRVisitor { - std::map cache; - - using IRVisitor::visit; - - bool no_overflow_int(const Type &t) { - return t.is_int() && t.bits() >= 32; - } - - ConstantInterval bounds(const Expr &e) { - return constant_integer_bounds(e, Scope::empty_scope(), &cache); - } - - void visit(const Add *op) override { - if (no_overflow_int(op->type) && - !op->type.can_represent(bounds(op->a) + bounds(op->b))) { - found = true; - } else { - IRVisitor::visit(op); - } - } - - void visit(const Sub *op) override { - if (no_overflow_int(op->type) && - !op->type.can_represent(bounds(op->a) - bounds(op->b))) { - found = true; - } else { - IRVisitor::visit(op); - } - } - - void visit(const Mul *op) override { - if (no_overflow_int(op->type) && - !op->type.can_represent(bounds(op->a) * bounds(op->b))) { - found = true; - } else { - IRVisitor::visit(op); - } - } - - void visit(const Div *op) override { - if (no_overflow_int(op->type) && - (bounds(op->a) / bounds(op->b)).contains(-1)) { - found = true; - } else { - IRVisitor::visit(op); - } - } - - void visit(const Cast *op) override { - if (no_overflow_int(op->type) && - !op->type.can_represent(bounds(op->value))) { - found = true; - } else { - IRVisitor::visit(op); - } - } - - void visit(const Call *op) override { - if (op->is_intrinsic({Call::shift_left, - Call::shift_right, - Call::rounding_shift_left, - Call::rounding_shift_right, - Call::widening_shift_left, - Call::widening_shift_right, - Call::mul_shift_right, - Call::rounding_mul_shift_right})) { - auto shift_bounds = bounds(op->args.back()); - if (!(shift_bounds > -op->type.bits() && shift_bounds < op->type.bits())) { - found = true; - } - } else if (op->is_intrinsic({Call::signed_integer_overflow})) { - found = true; - } - IRVisitor::visit(op); - } - - public: - bool found = false; - } checker; - - e.accept(&checker); - - return checker.found; -} + found_error |= check_lossless_cast(Bool(), e, make_reduce(Bool(), VectorReduce::Or)); -bool found_error = false; - -int test_one(uint32_t seed) { - std::mt19937 rng{seed}; - - buf_u8.fill(rng); - buf_i8.fill(rng); - - Expr e1 = random_expr(rng); - Expr simplified = simplify(e1); - - if (might_have_ub(e1) || - might_have_ub(simplified) || - might_have_ub(lower_intrinsics(simplified))) { - return 0; - } - - // We're also going to test constant_integer_bounds here. - ConstantInterval bounds = constant_integer_bounds(e1); - - Type target; - std::vector target_types = {UInt(32), Int(32), UInt(16), Int(16)}; - target = target_types[rng() % target_types.size()]; - Expr e2 = lossless_cast(target, e1); - - if (!e2.defined()) { - return 0; - } - - if (definitely_has_ub(e2)) { - std::cout << "lossless_cast introduced ub:\n" - << "seed = " << seed << "\n" - << "e1 = " << e1 << "\n" - << "e2 = " << e2 << "\n" - << "simplify(e1) = " << simplify(e1) << "\n" - << "simplify(e2) = " << simplify(e2) << "\n"; + if (found_error) { return 1; } - Func f; - f(x) = {cast(e1), cast(e2)}; - f.vectorize(x, 4, TailStrategy::RoundUp); - - Buffer out1(size), out2(size); - Pipeline p(f); - - // Check for signed integer overflow - // Module m = p.compile_to_module({}, "test"); - - p.realize({out1, out2}); - - for (int x = 0; x < size; x++) { - if (out1(x) != out2(x)) { - std::cout - << "lossless_cast failure\n" - << "seed = " << seed << "\n" - << "x = " << x << "\n" - << "buf_u8 = " << (int)buf_u8(x) << "\n" - << "buf_i8 = " << (int)buf_i8(x) << "\n" - << "out1 = " << out1(x) << "\n" - << "out2 = " << out2(x) << "\n" - << "Original: " << e1 << "\n" - << "Lossless cast: " << e2 << "\n"; - return 1; - } - } - - for (int x = 0; x < size; x++) { - if ((e1.type().is_int() && !bounds.contains(out1(x))) || - (e1.type().is_uint() && !bounds.contains((uint64_t)out1(x)))) { - Expr simplified = simplify(e1); - std::cout - << "constant_integer_bounds failure\n" - << "seed = " << seed << "\n" - << "x = " << x << "\n" - << "buf_u8 = " << (int)buf_u8(x) << "\n" - << "buf_i8 = " << (int)buf_i8(x) << "\n" - << "out1 = " << out1(x) << "\n" - << "Expression: " << e1 << "\n" - << "Bounds: " << bounds << "\n" - << "Simplified: " << simplified << "\n" - // If it's still out-of-bounds when the expression is - // simplified, that'll be easier to debug. - << "Bounds: " << constant_integer_bounds(simplified) << "\n"; - return 1; - } - } - - return 0; -} - -int fuzz_test(uint32_t root_seed) { - std::mt19937 seed_generator(root_seed); - - std::cout << "Fuzz testing with root seed " << root_seed << "\n"; - for (int i = 0; i < 1000; i++) { - auto s = seed_generator(); - std::cout << s << std::endl; - if (test_one(s)) { - return 1; - } - } - return 0; -} - -int main(int argc, char **argv) { - if (argc == 2) { - return test_one(atoi(argv[1])); - } - if (lossless_cast_test()) { - std::cout << "lossless_cast test failed!\n"; - return 1; - } - if (fuzz_test(time(NULL))) { - std::cout << "lossless_cast fuzz test failed!\n"; - return 1; - } std::cout << "Success!\n"; return 0; } diff --git a/test/correctness/random_expr_generator.h b/test/correctness/random_expr_generator.h deleted file mode 100644 index afde4ab4bbcc..000000000000 --- a/test/correctness/random_expr_generator.h +++ /dev/null @@ -1,389 +0,0 @@ -#include "Halide.h" - -#include -#include -#include -#include - -namespace Halide { -namespace Internal { - -using namespace std; -using namespace Halide; -using namespace Halide::Internal; - -class RandomExpressionGenerator { -public: - using RandomEngine = std::mt19937_64; - using make_bin_op_fn = Expr (*)(Expr, Expr); - - bool gen_cast = true; - bool gen_select = true; - bool gen_arithmetic = true; - bool gen_bitwise = true; - bool gen_bool_ops = true; - bool gen_reinterpret = true; - bool gen_broadcast_of_vector = true; - bool gen_ramp_of_vector = true; - bool gen_shuffles = true; - bool gen_vector_reduce = true; - - std::vector fuzz_types = {UInt(1), UInt(8), UInt(16), UInt(32), UInt(64), Int(8), Int(16), Int(32), Int(64)}; - std::vector> fuzz_vars{5}; - - template - decltype(auto) random_choice(RandomEngine &rng, T &&choices) { - std::uniform_int_distribution dist(0, std::size(choices) - 1); - return choices[dist(rng)]; - } - - Type random_scalar_type(RandomEngine &rng) { - return random_choice(rng, fuzz_types); - } - - int get_random_divisor(RandomEngine &rng, int x) { - vector divisors; - divisors.reserve(x); - for (int i = 2; i <= x; i++) { - if (x % i == 0) { - divisors.push_back(i); - } - } - return random_choice(rng, divisors); - } - - std::string fuzz_var(int i) { - return std::string(1, 'a' + i); - } - - Expr random_var(RandomEngine &rng, Type t) { - std::uniform_int_distribution dist(0, fuzz_vars.size() - 1); - int fuzz_count = dist(rng); - return cast(t, Variable::make(Int(32), fuzz_var(fuzz_count))); - } - - Type random_type(RandomEngine &rng, int width) { - Type t = random_choice(rng, fuzz_types); - if (width > 1) { - t = t.with_lanes(width); - } - return t; - } - - Expr random_const(RandomEngine &rng, Type t) { - int val = (int)((int8_t)(rng() & 0x0f)); - if (t.is_vector()) { - return Broadcast::make(cast(t.element_of(), val), t.lanes()); - } else { - return cast(t, val); - } - } - - static Expr make_absd(Expr a, Expr b) { - // random_expr() assumes that the result t is the same as the input t, - // which isn't true for all absd variants, so force the issue. - return cast(a.type(), absd(a, b)); - } - - static Expr make_bitwise_or(Expr a, Expr b) { - return a | b; - } - - static Expr make_bitwise_and(Expr a, Expr b) { - return a & b; - } - - static Expr make_bitwise_xor(Expr a, Expr b) { - return a ^ b; - } - - static Expr make_abs(Expr a, Expr) { - if (!a.type().is_uint()) { - return cast(a.type(), abs(a)); - } else { - return a; - } - } - - static Expr make_bitwise_not(Expr a, Expr) { - return ~a; - } - - static Expr make_shift_right(Expr a, Expr b) { - return a >> (b % a.type().bits()); - } - - Expr random_leaf(RandomEngine &rng, Type t, bool overflow_undef = false, bool imm_only = false) { - if (t.is_int() && t.bits() == 32) { - overflow_undef = true; - } - if (t.is_scalar()) { - if (!imm_only && (rng() & 1)) { - return random_var(rng, t); - } else { - if (overflow_undef) { - // For Int(32), we don't care about correctness during - // overflow, so just use numbers that are unlikely to - // overflow. - return cast(t, (int32_t)((int8_t)(rng() & 255))); - } else { - return cast(t, (int32_t)(rng())); - } - } - } else { - int lanes = get_random_divisor(rng, t.lanes()); - - if (rng() & 1) { - auto e1 = random_leaf(rng, t.with_lanes(t.lanes() / lanes), overflow_undef); - auto e2 = random_leaf(rng, t.with_lanes(t.lanes() / lanes), overflow_undef); - return Ramp::make(e1, e2, lanes); - } else { - auto e1 = random_leaf(rng, t.with_lanes(t.lanes() / lanes), overflow_undef); - return Broadcast::make(e1, lanes); - } - } - } - - // Expr random_expr(RandomEngine &rng, Type t, int depth, bool overflow_undef = false); - - Expr random_condition(RandomEngine &rng, Type t, int depth, bool maybe_scalar) { - static make_bin_op_fn make_bin_op[] = { - EQ::make, - NE::make, - LT::make, - LE::make, - GT::make, - GE::make, - }; - - if (maybe_scalar && (rng() & 1)) { - t = t.element_of(); - } - - Expr a = random_expr(rng, t, depth); - Expr b = random_expr(rng, t, depth); - return random_choice(rng, make_bin_op)(a, b); - } - - Expr random_expr(RandomEngine &rng, Type t, int depth, bool overflow_undef = false) { - if (t.is_int() && t.bits() == 32) { - overflow_undef = true; - } - - if (depth-- <= 0) { - return random_leaf(rng, t, overflow_undef); - } - - // Weight the choices to cover all Deinterleaver visit methods: - // Broadcast, Ramp, Cast, Reinterpret, Call (via abs), Shuffle, - // VectorReduce, Add/Sub/Min/Max (handled by default IRMutator) - std::vector> ops; - - // Leaf - ops.push_back([&]() -> Expr { - return random_leaf(rng, t); - }); - - if (gen_arithmetic) { - // Arithmetic - ops.push_back([&]() { - static make_bin_op_fn make_bin_op[] = { - // Arithmetic operations. - Add::make, - Sub::make, - Mul::make, - Min::make, - Max::make, - Div::make, - Mod::make, - make_absd, - make_abs}; - Expr a = random_expr(rng, t, depth, overflow_undef); - Expr b = random_expr(rng, t, depth, overflow_undef); - return random_choice(rng, make_bin_op)(a, b); - }); - } - if (gen_bitwise) { - // Bitwise - ops.push_back([&]() { - static make_bin_op_fn make_bin_op[] = { - make_bitwise_or, - make_bitwise_and, - make_bitwise_xor, - make_bitwise_not, - make_shift_right, // No shift left or we just keep testing integer overflow - }; - - Expr a = random_expr(rng, t, depth, overflow_undef); - Expr b = random_expr(rng, t, depth, overflow_undef); - return random_choice(rng, make_bin_op)(a, b); - }); - } - if (gen_bool_ops) { - // Boolean ops - ops.push_back([&]() { - static make_bin_op_fn make_bin_op[] = { - And::make, - Or::make, - }; - - // Boolean operations -- both sides must be cast to booleans, - // and then we must cast the result back to 't'. - Expr a = random_expr(rng, t, depth, overflow_undef); - Expr b = random_expr(rng, t, depth, overflow_undef); - Type bool_with_lanes = Bool(t.lanes()); - a = cast(bool_with_lanes, a); - b = cast(bool_with_lanes, b); - return cast(t, random_choice(rng, make_bin_op)(a, b)); - }); - } - if (gen_select) { - // Select - ops.push_back( - [&]() -> Expr { - auto c = random_condition(rng, t, depth, true); - auto e1 = random_expr(rng, t, depth, overflow_undef); - auto e2 = random_expr(rng, t, depth, overflow_undef); - return select(c, e1, e2); - }); - } - // Cast - if (gen_cast) { - ops.push_back([&]() { - // Get a random type that isn't `t` or int32 (int32 can overflow, and we don't care about that). - std::vector subtypes; - for (const Type &subtype : fuzz_types) { - if (subtype != t && subtype != Int(32)) { - subtypes.push_back(subtype); - } - } - Type subtype = random_choice(rng, subtypes).with_lanes(t.lanes()); - return Cast::make(t, random_expr(rng, subtype, depth, overflow_undef)); - }); - } - if (gen_reinterpret) { - // Reinterpret (different bit width, changes lane count) - ops.push_back([&]() -> Expr { - int total_bits = t.bits() * t.lanes(); - // Pick a different bit width that divides the total bits evenly - int bit_widths[] = {8, 16, 32, 64}; - vector valid_widths; - for (int bw : bit_widths) { - if (total_bits % bw == 0) { - valid_widths.push_back(bw); - } - } - // Should at least be able to preserve the existing bit width and change signedness. - internal_assert(!valid_widths.empty()); - int other_bits = random_choice(rng, valid_widths); - int other_lanes = total_bits / other_bits; - Type other = ((rng() & 1) ? Int(other_bits) : UInt(other_bits)).with_lanes(other_lanes); - Expr e = random_expr(rng, other, depth); - return Reinterpret::make(t, e); - }); - } - - if (gen_broadcast_of_vector) { - // Broadcast of vector - ops.push_back([&]() -> Expr { - if (t.lanes() != 1) { - int lanes = get_random_divisor(rng, t.lanes()); - auto e1 = random_expr(rng, t.with_lanes(t.lanes() / lanes), depth, overflow_undef); - return Broadcast::make(e1, lanes); - } - return random_expr(rng, t, depth, overflow_undef); - }); - } - - if (gen_ramp_of_vector) { - // Ramp - ops.push_back([&]() { - if (t.lanes() != 1) { - int lanes = get_random_divisor(rng, t.lanes()); - auto e1 = random_expr(rng, t.with_lanes(t.lanes() / lanes), depth, overflow_undef); - auto e2 = random_expr(rng, t.with_lanes(t.lanes() / lanes), depth, overflow_undef); - return Ramp::make(e1, e2, lanes); - } - return random_expr(rng, t, depth, overflow_undef); - }); - } - if (gen_bool_ops) { - ops.push_back([&]() { - if (t.is_bool()) { - auto e1 = random_expr(rng, t, depth); - return Not::make(e1); - } - return random_expr(rng, t, depth, overflow_undef); - }); - ops.push_back([&]() { - // When generating boolean expressions, maybe throw in a condition on non-bool types. - if (t.is_bool()) { - return random_condition(rng, random_type(rng, t.lanes()), depth, false); - } - return random_expr(rng, t, depth, overflow_undef); - }); - } - if (gen_shuffles) { - // Shuffle (interleave) - ops.push_back([&]() -> Expr { - if (t.lanes() >= 4 && t.lanes() % 2 == 0) { - int half = t.lanes() / 2; - Expr a = random_expr(rng, t.with_lanes(half), depth); - Expr b = random_expr(rng, t.with_lanes(half), depth); - return Shuffle::make_interleave({a, b}); - } - // Fall back to a simple expression - return random_expr(rng, t, depth); - }); - // Shuffle (concat) - ops.push_back([&]() -> Expr { - if (t.lanes() >= 4 && t.lanes() % 2 == 0) { - int half = t.lanes() / 2; - Expr a = random_expr(rng, t.with_lanes(half), depth); - Expr b = random_expr(rng, t.with_lanes(half), depth); - return Shuffle::make_concat({a, b}); - } - return random_expr(rng, t, depth); - }); - // Shuffle (slice) - ops.push_back([&]() -> Expr { - // Make a wider vector and slice it - if (t.lanes() <= 8) { - int wider = t.lanes() * 2; - Expr e = random_expr(rng, t.with_lanes(wider), depth); - // Slice: take every other element starting at 0 or 1 - int start = rng() & 1; - return Shuffle::make_slice(e, start, 2, t.lanes()); - } - return random_expr(rng, t, depth); - }); - } - if (gen_vector_reduce) { - // VectorReduce (only when we can make it work with lane counts) - ops.push_back([&]() -> Expr { - // Input has more lanes, output has t.lanes() lanes - // factor must divide input lanes, and input lanes = t.lanes() * factor - int factor = (rng() % 3) + 2; - int input_lanes = t.lanes() * factor; - if (input_lanes <= 32) { - VectorReduce::Operator ops[] = { - VectorReduce::Add, - VectorReduce::Min, - VectorReduce::Max, - }; - auto op = random_choice(rng, ops); - Expr val = random_expr(rng, t.with_lanes(input_lanes), depth); - internal_assert(val.type().lanes() == input_lanes) << val; - return VectorReduce::make(op, val, t.lanes()); - } - return random_expr(rng, t, depth); - }); - } - - Expr e = random_choice(rng, ops)(); - internal_assert(e.type() == t) << e.type() << " " << t << " " << e; - return e; - } -}; -} // namespace Internal -} // namespace Halide diff --git a/test/correctness/widening_lerp.cpp b/test/correctness/widening_lerp.cpp deleted file mode 100644 index e2b4af081db7..000000000000 --- a/test/correctness/widening_lerp.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#include "Halide.h" - -using namespace Halide; - -std::mt19937 rng(0); - -int main(int argc, char **argv) { - - int fuzz_seed = argc > 1 ? atoi(argv[1]) : time(nullptr); - rng.seed(fuzz_seed); - printf("Lerp test seed: %d\n", fuzz_seed); - - // Lerp lowering incorporates a cast. This test checks that a widening lerp - // is equal to the widened version of the lerp. - for (Type t1 : {UInt(8), UInt(16), UInt(32), Int(8), Int(16), Int(32), Float(32)}) { - if (rng() & 1) continue; - for (Type t2 : {UInt(8), UInt(16), UInt(32), Float(32)}) { - if (rng() & 1) continue; - for (Type t3 : {UInt(8), UInt(16), UInt(32), Int(8), Int(16), Int(32), Float(32)}) { - if (rng() & 1) continue; - Func f; - Var x; - f(x) = cast(t1, random_uint((int)rng())); - - Expr weight = cast(t2, f(x + 16)); - if (t2.is_float()) { - weight /= 256.f; - weight = clamp(weight, 0.f, 1.f); - } - - Expr zero_val = f(x); - Expr one_val = f(x + 8); - Expr lerped = lerp(zero_val, one_val, weight); - - Func cast_and_lerp, lerp_alone, cast_of_lerp; - cast_and_lerp(x) = cast(t3, lerped); - lerp_alone(x) = lerped; - cast_of_lerp(x) = cast(t3, lerp_alone(x)); - - RDom r(0, 32 * 1024); - Func check; - check() = maximum(abs(cast(cast_and_lerp(r)) - - cast(cast_of_lerp(r)))); - - f.compute_root().vectorize(x, 8, TailStrategy::RoundUp); - lerp_alone.compute_root().vectorize(x, 8, TailStrategy::RoundUp); - cast_and_lerp.compute_root().vectorize(x, 8, TailStrategy::RoundUp); - cast_of_lerp.compute_root().vectorize(x, 8, TailStrategy::RoundUp); - - double err = evaluate(check()); - - if (err > 1e-5) { - printf("Difference of lerp + cast and lerp alone is %f," - " which exceeds threshold for seed %d\n", - err, fuzz_seed); - return 1; - } - } - } - } - - printf("Success!\n"); - return 0; -} diff --git a/test/fuzz/CMakeLists.txt b/test/fuzz/CMakeLists.txt index 65376c7ebeb7..97b8372cf2ab 100644 --- a/test/fuzz/CMakeLists.txt +++ b/test/fuzz/CMakeLists.txt @@ -9,6 +9,9 @@ tests(GROUPS fuzz SOURCES bounds.cpp cse.cpp + lossless_cast.cpp + simplify.cpp + widening_lerp.cpp # By default, the libfuzzer harness runs with a timeout of 1200 seconds. # Let's dial that back: # - Do 1000 fuzz runs for each test. diff --git a/test/fuzz/fuzz_helpers.h b/test/fuzz/fuzz_helpers.h index 5b9740070be8..21017ec2ffa4 100644 --- a/test/fuzz/fuzz_helpers.h +++ b/test/fuzz/fuzz_helpers.h @@ -1,47 +1,35 @@ #ifndef HALIDE_FUZZ_HELPERS_H_ #define HALIDE_FUZZ_HELPERS_H_ +#include +#include +#include + #define HALIDE_FUZZER_BACKEND_STDLIB 0 #define HALIDE_FUZZER_BACKEND_LIBFUZZER 1 #ifndef HALIDE_FUZZER_BACKEND -#error "HALIDE_FUZZER_BACKEND not defined, defaulting to libFuzzer" +#error "HALIDE_FUZZER_BACKEND not defined. Set to either HALIDE_FUZZER_BACKEND_STDLIB or HALIDE_FUZZER_BACKEND_LIBFUZZER." #endif -/////////////////////////////////////////////////////////////////////////////// - -#include -#include -#include -#include -#include - #if HALIDE_FUZZER_BACKEND == HALIDE_FUZZER_BACKEND_LIBFUZZER #include "fuzzer/FuzzedDataProvider.h" // IWYU pragma: export #elif HALIDE_FUZZER_BACKEND == HALIDE_FUZZER_BACKEND_STDLIB -#include "halide_fuzz_main.h" +#include "halide_fuzz_main.h" // IWYU pragma: export #include +#else +#error "HALIDE_FUZZER_BACKEND must be set to either HALIDE_FUZZER_BACKEND_STDLIB or HALIDE_FUZZER_BACKEND_LIBFUZZER." #endif namespace Halide { -#if HALIDE_FUZZER_BACKEND == HALIDE_FUZZER_BACKEND_LIBFUZZER -class FuzzingContext : public FuzzedDataProvider { -public: - using FuzzedDataProvider::FuzzedDataProvider; - template - T PickValueInVector(std::vector &vec) { - return vec[ConsumeIntegralInRange(0, vec.size() - 1)]; - } -}; -#elif HALIDE_FUZZER_BACKEND == HALIDE_FUZZER_BACKEND_STDLIB -// IMPORTANT: we don't use std::*_distribution because they are not portable across standard libraries -class FuzzingContext { +#if HALIDE_FUZZER_BACKEND == HALIDE_FUZZER_BACKEND_STDLIB +class FuzzedDataProvider { public: using RandomEngine = std::mt19937_64; using SeedType = RandomEngine::result_type; - explicit FuzzingContext(SeedType seed) : rng(seed) { + explicit FuzzedDataProvider(SeedType seed) : rng(seed) { } template @@ -64,13 +52,13 @@ class FuzzingContext { } template - T PickValueInVector(std::vector &vec) { - return vec[ConsumeIntegralInRange(static_cast(0), vec.size() - 1)]; + auto PickValueInArray(T &array) -> decltype(auto) { + return array[ConsumeIntegralInRange(static_cast(0), std::size(array) - 1)]; } template - auto PickValueInArray(T &array) -> decltype(auto) { - return array[ConsumeIntegralInRange(static_cast(0), std::size(array) - 1)]; + auto PickValueInArray(const std::initializer_list &list) -> decltype(auto) { + return *(list.begin() + ConsumeIntegralInRange(static_cast(0), std::size(list) - 1)); } private: @@ -78,6 +66,20 @@ class FuzzingContext { }; #endif +class FuzzingContext : public FuzzedDataProvider { +public: + using FuzzedDataProvider::FuzzedDataProvider; + + template + T PickValueInVector(std::vector &vec) { + return vec[ConsumeIntegralInRange(0, vec.size() - 1)]; + } + + auto operator()() -> decltype(auto) { + return ConsumeIntegral(); + } +}; + } // namespace Halide #if HALIDE_FUZZER_BACKEND == HALIDE_FUZZER_BACKEND_LIBFUZZER diff --git a/test/fuzz/lossless_cast.cpp b/test/fuzz/lossless_cast.cpp new file mode 100644 index 000000000000..ed4330b76f38 --- /dev/null +++ b/test/fuzz/lossless_cast.cpp @@ -0,0 +1,225 @@ +#include "fuzz_helpers.h" +#include "random_expr_generator.h" +#include + +using namespace Halide; +using namespace Halide::Internal; + +namespace { + +bool definitely_has_ub(Expr e) { + e = simplify(e); + + class HasOverflow : public IRVisitor { + void visit(const Call *op) override { + if (op->is_intrinsic({Call::signed_integer_overflow})) { + found = true; + } + IRVisitor::visit(op); + } + + public: + bool found = false; + } has_overflow; + e.accept(&has_overflow); + + return has_overflow.found; +} + +bool might_have_ub(Expr e) { + class MightOverflow : public IRVisitor { + std::map cache; + + using IRVisitor::visit; + + bool no_overflow_int(const Type &t) { + return t.is_int() && t.bits() >= 32; + } + + ConstantInterval bounds(const Expr &e) { + return constant_integer_bounds(e, Scope::empty_scope(), &cache); + } + + void visit(const Add *op) override { + if (no_overflow_int(op->type) && + !op->type.can_represent(bounds(op->a) + bounds(op->b))) { + found = true; + } else { + IRVisitor::visit(op); + } + } + + void visit(const Sub *op) override { + if (no_overflow_int(op->type) && + !op->type.can_represent(bounds(op->a) - bounds(op->b))) { + found = true; + } else { + IRVisitor::visit(op); + } + } + + void visit(const Mul *op) override { + if (no_overflow_int(op->type) && + !op->type.can_represent(bounds(op->a) * bounds(op->b))) { + found = true; + } else { + IRVisitor::visit(op); + } + } + + void visit(const Div *op) override { + if (no_overflow_int(op->type) && + (bounds(op->a) / bounds(op->b)).contains(-1)) { + found = true; + } else { + IRVisitor::visit(op); + } + } + + void visit(const Cast *op) override { + if (no_overflow_int(op->type) && + !op->type.can_represent(bounds(op->value))) { + found = true; + } else { + IRVisitor::visit(op); + } + } + + void visit(const Call *op) override { + if (op->is_intrinsic({Call::shift_left, + Call::shift_right, + Call::rounding_shift_left, + Call::rounding_shift_right, + Call::widening_shift_left, + Call::widening_shift_right, + Call::mul_shift_right, + Call::rounding_mul_shift_right})) { + auto shift_bounds = bounds(op->args.back()); + if (!(shift_bounds > -op->type.bits() && shift_bounds < op->type.bits())) { + found = true; + } + } else if (op->is_intrinsic({Call::signed_integer_overflow})) { + found = true; + } + IRVisitor::visit(op); + } + + public: + bool found = false; + } checker; + + e.accept(&checker); + + return checker.found; +} + +} // namespace + +FUZZ_TEST(lossless_cast, FuzzingContext &fuzz) { + constexpr int size = 1024; + Buffer buf_u8(size, "buf_u8"); + Buffer buf_i8(size, "buf_i8"); + Var x{"x"}; + + buf_u8.fill(fuzz); + buf_i8.fill(fuzz); + + RandomExpressionGenerator reg{ + fuzz, + { + buf_u8(x), + buf_i8(x), + cast(fuzz.ConsumeIntegral()), + cast(fuzz.ConsumeIntegral()), + cast(fuzz.ConsumeIntegral()), + cast(fuzz.ConsumeIntegral()), + }}; + // Scalar integer types only, no bool. TODO: Int64 fails + reg.fuzz_types = {UInt(8), UInt(16), UInt(32), Int(8), Int(16), Int(32)}; + // Scalar only, disable vector-specific operations + reg.gen_broadcast_of_vector = false; + reg.gen_ramp_of_vector = false; + reg.gen_shuffles = false; + reg.gen_vector_reduce = false; + reg.gen_reinterpret = false; + + constexpr int depth = 5; + Expr e1 = reg.random_expr(reg.random_type(), depth); + + Expr simplified = simplify(e1); + + if (might_have_ub(e1) || + might_have_ub(simplified) || + might_have_ub(lower_intrinsics(simplified))) { + return 0; + } + + // We're also going to test constant_integer_bounds here. + ConstantInterval bounds = constant_integer_bounds(e1); + + std::vector target_types = {UInt(32), Int(32), UInt(16), Int(16)}; + Type target = fuzz.PickValueInVector(target_types); + Expr e2 = lossless_cast(target, e1); + + if (!e2.defined()) { + return 0; + } + + if (definitely_has_ub(e2)) { + std::cerr << "lossless_cast introduced ub:\n" + << "e1 = " << e1 << "\n" + << "e2 = " << e2 << "\n" + << "simplify(e1) = " << simplify(e1) << "\n" + << "simplify(e2) = " << simplify(e2) << "\n"; + return 1; + } + + Func f; + f(x) = {cast(e1), cast(e2)}; + f.vectorize(x, 4, TailStrategy::RoundUp); + + Buffer out1(size), out2(size); + Pipeline p(f); + + // Check for signed integer overflow + // Module m = p.compile_to_module({}, "test"); + + p.realize({out1, out2}); + + for (int x = 0; x < size; x++) { + if (out1(x) != out2(x)) { + std::cerr + << "lossless_cast failure\n" + << "x = " << x << "\n" + << "buf_u8 = " << (int)buf_u8(x) << "\n" + << "buf_i8 = " << (int)buf_i8(x) << "\n" + << "out1 = " << out1(x) << "\n" + << "out2 = " << out2(x) << "\n" + << "Original: " << e1 << "\n" + << "Lossless cast: " << e2 << "\n"; + return 1; + } + } + + for (int x = 0; x < size; x++) { + if ((e1.type().is_int() && !bounds.contains(out1(x))) || + (e1.type().is_uint() && !bounds.contains((uint64_t)out1(x)))) { + Expr simplified = simplify(e1); + std::cerr + << "constant_integer_bounds failure\n" + << "x = " << x << "\n" + << "buf_u8 = " << (int)buf_u8(x) << "\n" + << "buf_i8 = " << (int)buf_i8(x) << "\n" + << "out1 = " << out1(x) << "\n" + << "Expression: " << e1 << "\n" + << "Bounds: " << bounds << "\n" + << "Simplified: " << simplified << "\n" + // If it's still out-of-bounds when the expression is + // simplified, that'll be easier to debug. + << "Bounds: " << constant_integer_bounds(simplified) << "\n"; + return 1; + } + } + + return 0; +} diff --git a/test/fuzz/random_expr_generator.h b/test/fuzz/random_expr_generator.h new file mode 100644 index 000000000000..4926736e0339 --- /dev/null +++ b/test/fuzz/random_expr_generator.h @@ -0,0 +1,450 @@ +#include "Halide.h" + +#include +#include +#include + +#include "fuzz_helpers.h" + +namespace Halide { +namespace Internal { + +using namespace std; +using namespace Halide; +using namespace Halide::Internal; + +class RandomExpressionGenerator { +public: + using make_bin_op_fn = Expr (*)(Expr, Expr); + + // keep-sorted start + bool gen_arithmetic = true; + bool gen_bitwise = true; + bool gen_bool_ops = true; + bool gen_broadcast_of_vector = true; + bool gen_cast = true; + bool gen_cse = true; + bool gen_intrinsics = true; + bool gen_ramp_of_vector = true; + bool gen_reinterpret = true; + bool gen_select = true; + bool gen_shuffles = true; + bool gen_vector_reduce = true; + // keep-sorted end + + FuzzingContext &fuzz; + + std::vector fuzz_types = {UInt(1), UInt(8), UInt(16), UInt(32), UInt(64), Int(8), Int(16), Int(32), Int(64)}; + std::vector atoms; + + explicit RandomExpressionGenerator(FuzzingContext &fuzz, std::vector atoms) + : fuzz(fuzz), atoms(std::move(atoms)) { + } + + int get_random_divisor(int x) { + vector divisors; + divisors.reserve(x); + for (int i = 2; i <= x; i++) { + if (x % i == 0) { + divisors.push_back(i); + } + } + return fuzz.PickValueInVector(divisors); + } + + Expr random_var(Type t) { + return cast(t, fuzz.PickValueInVector(atoms)); + } + + Type random_type(int width = 1) { + Type t = fuzz.PickValueInVector(fuzz_types); + if (width > 1) { + t = t.with_lanes(width); + } + return t; + } + + Expr random_const(Type t) const { + int val = fuzz.ConsumeIntegralInRange(0, 0x0f); + if (t.is_vector()) { + return Broadcast::make(cast(t.element_of(), val), t.lanes()); + } else { + return cast(t, val); + } + } + + static Expr make_absd(Expr a, Expr b) { + // random_expr() assumes that the result t is the same as the input t, + // which isn't true for all absd variants, so force the issue. + return cast(a.type(), absd(a, b)); + } + + static Expr make_bitwise_or(Expr a, Expr b) { + return a | b; + } + + static Expr make_bitwise_and(Expr a, Expr b) { + return a & b; + } + + static Expr make_bitwise_xor(Expr a, Expr b) { + return a ^ b; + } + + static Expr make_abs(Expr a, Expr) { + if (!a.type().is_uint()) { + return cast(a.type(), abs(a)); + } else { + return a; + } + } + + static Expr make_bitwise_not(Expr a, Expr) { + return ~a; + } + + static Expr make_shift_right(Expr a, Expr b) { + return a >> (b % a.type().bits()); + } + + Expr random_leaf(Type t, bool overflow_undef = false, bool imm_only = false) { + if (t.is_int() && t.bits() == 32) { + overflow_undef = true; + } + if (t.is_scalar()) { + if (!imm_only && fuzz.ConsumeBool()) { + return random_var(t); + } else { + if (overflow_undef) { + // For Int(32), we don't care about correctness during + // overflow, so just use numbers that are unlikely to + // overflow. + return cast(t, fuzz.ConsumeIntegralInRange(0, 255)); + } else { + return cast(t, fuzz.ConsumeIntegral()); + } + } + } else { + int lanes = get_random_divisor(t.lanes()); + + if (fuzz.ConsumeBool()) { + auto e1 = random_leaf(t.with_lanes(t.lanes() / lanes), overflow_undef); + auto e2 = random_leaf(t.with_lanes(t.lanes() / lanes), overflow_undef); + return Ramp::make(e1, e2, lanes); + } else { + auto e1 = random_leaf(t.with_lanes(t.lanes() / lanes), overflow_undef); + return Broadcast::make(e1, lanes); + } + } + } + + // Expr random_expr( Type t, int depth, bool overflow_undef = false); + + Expr random_condition(Type t, int depth, bool maybe_scalar) { + static make_bin_op_fn make_bin_op[] = { + EQ::make, + NE::make, + LT::make, + LE::make, + GT::make, + GE::make, + }; + + if (maybe_scalar && fuzz.ConsumeBool()) { + t = t.element_of(); + } + + Expr a = random_expr(t, depth); + Expr b = random_expr(t, depth); + return fuzz.PickValueInArray(make_bin_op)(a, b); + } + + Expr random_expr(Type t, int depth, bool overflow_undef = false) { + if (t.is_int() && t.bits() == 32) { + overflow_undef = true; + } + + if (depth-- <= 0) { + return random_leaf(t, overflow_undef); + } + + // Weight the choices to cover all Deinterleaver visit methods: + // Broadcast, Ramp, Cast, Reinterpret, Call (via abs), Shuffle, + // VectorReduce, Add/Sub/Min/Max (handled by default IRMutator) + std::vector> ops; + + // Leaf + ops.emplace_back([&] { + return random_leaf(t); + }); + + if (gen_arithmetic) { + // Arithmetic + ops.emplace_back([&] { + static make_bin_op_fn make_bin_op[] = { + // Arithmetic operations. + Add::make, + Sub::make, + Mul::make, + Min::make, + Max::make, + Div::make, + Mod::make, + make_absd, + make_abs}; + Expr a = random_expr(t, depth, overflow_undef); + Expr b = random_expr(t, depth, overflow_undef); + return fuzz.PickValueInArray(make_bin_op)(a, b); + }); + } + if (gen_bitwise) { + // Bitwise + ops.emplace_back([&] { + static make_bin_op_fn make_bin_op[] = { + make_bitwise_or, + make_bitwise_and, + make_bitwise_xor, + make_bitwise_not, + make_shift_right, // No shift left or we just keep testing integer overflow + }; + + Expr a = random_expr(t, depth, overflow_undef); + Expr b = random_expr(t, depth, overflow_undef); + return fuzz.PickValueInArray(make_bin_op)(a, b); + }); + } + if (gen_bool_ops) { + // Boolean ops + ops.emplace_back([&] { + static make_bin_op_fn make_bin_op[] = { + And::make, + Or::make, + }; + + // Boolean operations -- both sides must be cast to booleans, + // and then we must cast the result back to 't'. + Expr a = random_expr(t, depth, overflow_undef); + Expr b = random_expr(t, depth, overflow_undef); + Type bool_with_lanes = Bool(t.lanes()); + a = cast(bool_with_lanes, a); + b = cast(bool_with_lanes, b); + return cast(t, fuzz.PickValueInArray(make_bin_op)(a, b)); + }); + } + if (gen_select) { + // Select + ops.emplace_back([&] { + auto c = random_condition(t, depth, true); + auto e1 = random_expr(t, depth, overflow_undef); + auto e2 = random_expr(t, depth, overflow_undef); + return select(c, e1, e2); + }); + } + // Cast + if (gen_cast) { + ops.emplace_back([&] { + // Get a random type that isn't `t` or int32 (int32 can overflow, and we don't care about that). + std::vector subtypes; + for (const Type &subtype : fuzz_types) { + if (subtype != t && subtype != Int(32)) { + subtypes.push_back(subtype); + } + } + Type subtype = fuzz.PickValueInVector(subtypes).with_lanes(t.lanes()); + return Cast::make(t, random_expr(subtype, depth, overflow_undef)); + }); + } + if (gen_reinterpret) { + // Reinterpret (different bit width, changes lane count) + ops.emplace_back([&] { + int total_bits = t.bits() * t.lanes(); + // Pick a different bit width that divides the total bits evenly + int bit_widths[] = {8, 16, 32, 64}; + vector valid_widths; + for (int bw : bit_widths) { + if (total_bits % bw == 0) { + valid_widths.push_back(bw); + } + } + // Should at least be able to preserve the existing bit width and change signedness. + internal_assert(!valid_widths.empty()); + int other_bits = fuzz.PickValueInVector(valid_widths); + int other_lanes = total_bits / other_bits; + Type other = (fuzz.ConsumeBool() ? Int(other_bits) : UInt(other_bits)).with_lanes(other_lanes); + Expr e = random_expr(other, depth); + return Reinterpret::make(t, e); + }); + } + + if (gen_broadcast_of_vector) { + // Broadcast of vector + ops.emplace_back([&] { + if (t.lanes() != 1) { + int lanes = get_random_divisor(t.lanes()); + auto e1 = random_expr(t.with_lanes(t.lanes() / lanes), depth, overflow_undef); + return Broadcast::make(e1, lanes); + } + return random_expr(t, depth, overflow_undef); + }); + } + + if (gen_ramp_of_vector) { + // Ramp + ops.emplace_back([&] { + if (t.lanes() != 1) { + int lanes = get_random_divisor(t.lanes()); + auto e1 = random_expr(t.with_lanes(t.lanes() / lanes), depth, overflow_undef); + auto e2 = random_expr(t.with_lanes(t.lanes() / lanes), depth, overflow_undef); + return Ramp::make(e1, e2, lanes); + } + return random_expr(t, depth, overflow_undef); + }); + } + if (gen_bool_ops) { + ops.emplace_back([&] { + if (t.is_bool()) { + auto e1 = random_expr(t, depth); + return Not::make(e1); + } + return random_expr(t, depth, overflow_undef); + }); + ops.emplace_back([&] { + // When generating boolean expressions, maybe throw in a condition on non-bool types. + if (t.is_bool()) { + return random_condition(random_type(t.lanes()), depth, false); + } + return random_expr(t, depth, overflow_undef); + }); + } + if (gen_shuffles) { + // Shuffle (interleave) + ops.emplace_back([&] { + if (t.lanes() >= 4 && t.lanes() % 2 == 0) { + int half = t.lanes() / 2; + Expr a = random_expr(t.with_lanes(half), depth); + Expr b = random_expr(t.with_lanes(half), depth); + return Shuffle::make_interleave({a, b}); + } + // Fall back to a simple expression + return random_expr(t, depth); + }); + // Shuffle (concat) + ops.emplace_back([&] { + if (t.lanes() >= 4 && t.lanes() % 2 == 0) { + int half = t.lanes() / 2; + Expr a = random_expr(t.with_lanes(half), depth); + Expr b = random_expr(t.with_lanes(half), depth); + return Shuffle::make_concat({a, b}); + } + return random_expr(t, depth); + }); + // Shuffle (slice) + ops.emplace_back([&] { + // Make a wider vector and slice it + if (t.lanes() <= 8) { + int wider = t.lanes() * 2; + Expr e = random_expr(t.with_lanes(wider), depth); + // Slice: take every other element starting at 0 or 1 + int start = fuzz.ConsumeIntegralInRange(0, 1); + return Shuffle::make_slice(e, start, 2, t.lanes()); + } + return random_expr(t, depth); + }); + } + if (gen_vector_reduce) { + // VectorReduce (only when we can make it work with lane counts) + ops.emplace_back([&] { + // Input has more lanes, output has t.lanes() lanes + // factor must divide input lanes, and input lanes = t.lanes() * factor + int factor = fuzz.ConsumeIntegralInRange(2, 4); + int input_lanes = t.lanes() * factor; + if (input_lanes <= 32) { + VectorReduce::Operator ops[] = { + VectorReduce::Add, + VectorReduce::Min, + VectorReduce::Max, + }; + auto op = fuzz.PickValueInArray(ops); + Expr val = random_expr(t.with_lanes(input_lanes), depth); + internal_assert(val.type().lanes() == input_lanes) << val; + return VectorReduce::make(op, val, t.lanes()); + } + return random_expr(t, depth); + }); + } + if (gen_intrinsics && t.bits() >= 8) { + // Fixed-point and intrinsic operations (from lossless_cast fuzzer) + ops.emplace_back([&] { + bool may_widen = t.bits() < 32; // TODO: uint64 is broken + bool has_narrow = t.bits() >= 16; + Type nt = has_narrow ? t.narrow() : t; + + std::vector> choices; + + // Halving ops + choices.emplace_back([&] { return halving_add(random_expr(t, depth, overflow_undef), random_expr(t, depth, overflow_undef)); }); + choices.emplace_back([&] { return rounding_halving_add(random_expr(t, depth, overflow_undef), random_expr(t, depth, overflow_undef)); }); + choices.emplace_back([&] { return halving_sub(random_expr(t, depth, overflow_undef), random_expr(t, depth, overflow_undef)); }); + + // Saturating ops + choices.emplace_back([&] { return saturating_add(random_expr(t, depth, overflow_undef), random_expr(t, depth, overflow_undef)); }); + choices.emplace_back([&] { return saturating_sub(random_expr(t, depth, overflow_undef), random_expr(t, depth, overflow_undef)); }); + + // Count ops + choices.emplace_back([&] { return count_leading_zeros(random_expr(t, depth, overflow_undef)); }); + choices.emplace_back([&] { return count_trailing_zeros(random_expr(t, depth, overflow_undef)); }); + + // Rounding shift ops + choices.emplace_back([&] { return rounding_shift_right(random_expr(t, depth, overflow_undef), random_expr(t, depth, overflow_undef)); }); + choices.emplace_back([&] { return rounding_shift_left(random_expr(t, depth, overflow_undef), random_expr(t, depth, overflow_undef)); }); + + // Widening ops: inputs are t.narrow(), output is t + if (has_narrow) { + choices.emplace_back([&] { return widening_add(random_expr(nt, depth, overflow_undef), random_expr(nt, depth, overflow_undef)); }); + choices.emplace_back([&] { return widening_mul(random_expr(nt, depth, overflow_undef), random_expr(nt, depth, overflow_undef)); }); + } + + // Widening sub always returns signed + if (has_narrow && t.is_int()) { + choices.emplace_back([&] { return widening_sub(random_expr(nt, depth, overflow_undef), random_expr(nt, depth, overflow_undef)); }); + } + + // Widen-right ops: a is type t, b is type t.narrow(), output is type t + if (has_narrow) { + choices.emplace_back([&] { return widen_right_add(random_expr(t, depth, overflow_undef), random_expr(nt, depth, overflow_undef)); }); + choices.emplace_back([&] { return widen_right_sub(random_expr(t, depth, overflow_undef), random_expr(nt, depth, overflow_undef)); }); + choices.emplace_back([&] { return widen_right_mul(random_expr(t, depth, overflow_undef), random_expr(nt, depth, overflow_undef)); }); + } + + // mul_shift_right / rounding_mul_shift_right + if (may_widen) { + choices.emplace_back([&] { + Expr a = random_expr(t, depth, overflow_undef); + Expr b = random_expr(t, depth, overflow_undef); + Expr c = cast(t.with_code(halide_type_uint), random_expr(t, depth, overflow_undef)); + return mul_shift_right(a, b, c); + }); + choices.emplace_back([&] { + Expr a = random_expr(t, depth, overflow_undef); + Expr b = random_expr(t, depth, overflow_undef); + Expr c = cast(t.with_code(halide_type_uint), random_expr(t, depth, overflow_undef)); + return rounding_mul_shift_right(a, b, c); + }); + } + + return fuzz.PickValueInVector(choices)(); + }); + } + if (gen_cse) { + ops.emplace_back([&] { + return common_subexpression_elimination(random_expr(t, depth, overflow_undef)); + }); + } + + Expr e = fuzz.PickValueInVector(ops)(); + internal_assert(e.type() == t) << e.type() << " " << t << " " << e; + return e; + } +}; +} // namespace Internal +} // namespace Halide diff --git a/test/fuzz/simplify.cpp b/test/fuzz/simplify.cpp new file mode 100644 index 000000000000..061f2641fa2d --- /dev/null +++ b/test/fuzz/simplify.cpp @@ -0,0 +1,168 @@ +#include "Halide.h" +#include + +#include "fuzz_helpers.h" +#include "random_expr_generator.h" + +// Test the simplifier in Halide by testing for equivalence of randomly generated expressions. +namespace { + +using std::map; +using std::string; +using namespace Halide; +using namespace Halide::Internal; + +bool test_simplification(Expr a, Expr b, const map &vars) { + if (equal(a, b) && !a.same_as(b)) { + std::cerr << "Simplifier created new IR node but made no changes:\n" + << a << "\n"; + return false; + } + if (Expr sb = simplify(b); !equal(b, sb)) { + // Test all sub-expressions in pre-order traversal to minimize + bool found_failure = false; + mutate_with(a, [&](auto *self, const Expr &e) { + self->mutate_base(e); + Expr s = simplify(e); + Expr ss = simplify(s); + if (!found_failure && !equal(s, ss)) { + std::cerr << "Idempotency failure\n " + << e << "\n -> " + << s << "\n -> " + << ss << "\n"; + // These are broken out below to make it easier to parse any logging + // added to the simplifier to debug the failure. + std::cerr << "---------------------------------\n" + << "Begin simplification of original:\n" + << simplify(e) << "\n"; + std::cerr << "---------------------------------\n" + << "Begin resimplification of result:\n" + << simplify(s) << "\n" + << "---------------------------------\n"; + + found_failure = true; + } + return e; + }); + return false; + } + + Expr a_v = simplify(substitute(vars, a)); + Expr b_v = simplify(substitute(vars, b)); + // If the simplifier didn't produce constants, there must be + // undefined behavior in this expression. Ignore it. + if (!Internal::is_const(a_v) || !Internal::is_const(b_v)) { + return true; + } + if (!equal(a_v, b_v)) { + std::cerr << "Simplified Expr is not equal() to Original Expr!\n"; + + for (const auto &[var, val] : vars) { + std::cerr << "Var " << var << " = " << val << "\n"; + } + + std::cerr << "Original Expr is: " << a << "\n"; + std::cerr << "Simplified Expr is: " << b << "\n"; + std::cerr << " " << a << " -> " << a_v << "\n"; + std::cerr << " " << b << " -> " << b_v << "\n"; + return false; + } + + return true; +} + +bool test_expression(RandomExpressionGenerator ®, Expr test, int samples) { + Expr simplified = simplify(test); + + map vars; + for (const auto &atom : reg.atoms) { + if (const Variable *v = atom.as()) { + vars[v->name] = atom; + } + } + + for (int i = 0; i < samples; i++) { + for (auto &[var, val] : vars) { + constexpr size_t kMaxLeafIterations = 10000; + // Don't let the random leaf depend on v itself. + size_t iterations = 0; + do { + val = reg.random_leaf(Int(32), true); + iterations++; + } while (expr_uses_var(val, var) && iterations < kMaxLeafIterations); + } + + if (!test_simplification(test, simplified, vars)) { + return false; + } + } + return true; +} + +Expr simplify_at_depth(int limit, const Expr &in) { + return mutate_with(in, [&](auto *self, const Expr &e) { + if (limit == 0) { + return simplify(e); + } + limit--; + Expr new_e = self->mutate_base(e); + limit++; + return new_e; + }); +} + +} // namespace + +FUZZ_TEST(simplify, FuzzingContext &fuzz) { + // Depth of the randomly generated expression trees. + constexpr int depth = 6; + // Number of samples to test the generated expressions for. + constexpr int samples = 3; + // Number of samples to test the generated expressions for during minimization. + constexpr int samples_during_minimization = 100; + + RandomExpressionGenerator reg{ + fuzz, + { + Param("a0"), + Param("a1"), + Param("a2"), + Param("a3"), + Param("a4"), + }}; + // FIXME: These need to be disabled (otherwise crashes and/or failures): + // reg.gen_ramp_of_vector = false; + // reg.gen_broadcast_of_vector = false; + reg.gen_vector_reduce = false; + reg.gen_reinterpret = false; + reg.gen_shuffles = false; + + int width = fuzz.PickValueInArray({1, 2, 3, 4, 6, 8}); + Expr test = reg.random_expr(reg.random_type(width), depth); + + if (!test_expression(reg, test, samples)) { + // Failure. Find the minimal subexpression that failed. + std::cerr << "Testing subexpressions...\n"; + bool found_failure = false; + test = mutate_with(test, [&](auto *self, const Expr &e) { + self->mutate_base(e); + if (e.type().bits() && !found_failure) { + for (int i = 1; i < 4 && !found_failure; i++) { + Expr limited = simplify_at_depth(i, e); + found_failure = !test_expression(reg, limited, samples_during_minimization); + if (found_failure) { + return limited; + } + } + if (!found_failure) { + found_failure = !test_expression(reg, e, samples_during_minimization); + } + } + return e; + }); + std::cerr << "Final test case: " << test << "\n"; + return 1; + } + + return 0; +} diff --git a/test/fuzz/widening_lerp.cpp b/test/fuzz/widening_lerp.cpp new file mode 100644 index 000000000000..1df0aa00f753 --- /dev/null +++ b/test/fuzz/widening_lerp.cpp @@ -0,0 +1,51 @@ +#include "fuzz_helpers.h" + +#include "Halide.h" +using namespace Halide; + +FUZZ_TEST(widening_lerp, FuzzingContext &fuzz) { + // Lerp lowering incorporates a cast. This test checks that a widening lerp + // is equal to the widened version of the lerp. + + Type t1 = fuzz.PickValueInArray({UInt(8), UInt(16), UInt(32), Int(8), Int(16), Int(32), Float(32)}); + Type t2 = fuzz.PickValueInArray({UInt(8), UInt(16), UInt(32), Float(32)}); + Type t3 = fuzz.PickValueInArray({UInt(8), UInt(16), UInt(32), Int(8), Int(16), Int(32), Float(32)}); + + Func f; + Var x; + f(x) = cast(t1, random_uint(fuzz.ConsumeIntegral())); + + Expr weight = cast(t2, f(x + 16)); + if (t2.is_float()) { + weight /= 256.f; + weight = clamp(weight, 0.f, 1.f); + } + + Expr zero_val = f(x); + Expr one_val = f(x + 8); + Expr lerped = lerp(zero_val, one_val, weight); + + Func cast_and_lerp, lerp_alone, cast_of_lerp; + cast_and_lerp(x) = cast(t3, lerped); + lerp_alone(x) = lerped; + cast_of_lerp(x) = cast(t3, lerp_alone(x)); + + RDom r(0, 32 * 1024); + Func check; + check() = maximum(abs(cast(cast_and_lerp(r)) - + cast(cast_of_lerp(r)))); + + f.compute_root().vectorize(x, 8, TailStrategy::RoundUp); + lerp_alone.compute_root().vectorize(x, 8, TailStrategy::RoundUp); + cast_and_lerp.compute_root().vectorize(x, 8, TailStrategy::RoundUp); + cast_of_lerp.compute_root().vectorize(x, 8, TailStrategy::RoundUp); + + double err = evaluate(check()); + + if (err > 1e-5) { + std::cerr << "Difference of lerp + cast and lerp alone is " << err << "\n"; + return 1; + } + + return 0; +}