Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
1399d13
Let can_prove predicates use the simplifier's known facts
mcourteaux Aug 26, 2026
6fa6d43
Make fact lookup aware of comparison direction and strictness
mcourteaux Aug 26, 2026
bd22e41
Don't re-enter fact-driven rewrite rules from inside a can_prove
mcourteaux Aug 26, 2026
0c73eea
Express the can_prove re-entry guard as a depth limit
mcourteaux Aug 26, 2026
e6866e7
Add a non-recursive known_true predicate for rewrite rules
mcourteaux Aug 27, 2026
6664311
Fix parenthesis of Simplify_Div.
mcourteaux Aug 27, 2026
8af31a8
Guard against can_prove recursion at its source
mcourteaux Aug 27, 2026
abda89a
Fall back to fact lookup at the can_prove depth cap
mcourteaux Aug 27, 2026
f7bf9b7
Use a direct fact lookup at the can_prove depth cap, not a tree walk
mcourteaux Aug 27, 2026
1e9eb9c
Answer ordering questions from constant bounds on differences, not IR
mcourteaux Sep 4, 2026
d82b2ec
Lower the can_prove depth limit to two
mcourteaux Sep 5, 2026
01a2108
Let known_difference reason without facts
mcourteaux Sep 5, 2026
9ace36b
Test the fact-free reasoning in known_difference
mcourteaux Sep 5, 2026
5b7483c
Reject known_difference candidates on a summary before comparing Exprs
mcourteaux Sep 5, 2026
74e1d92
Gate the difference rules on the difference table, not on any fact
mcourteaux Sep 5, 2026
185ead5
Reject a difference lookup against the whole table in one test
mcourteaux Sep 5, 2026
260cc16
Key same-type pairs by their kind rather than collapsing them onto on…
mcourteaux Sep 5, 2026
1915e46
Don't order a min or max from the condition of an if
mcourteaux Sep 5, 2026
fa3b132
Order a min or max from an if's condition only once regions are derived
mcourteaux Sep 5, 2026
9e825cd
Review fixes: type-gate the structural bound, and drop what nothing r…
mcourteaux Sep 6, 2026
8be980b
Gate every fact on the phase, not just the ones from ifs
mcourteaux Sep 6, 2026
0d05e92
Test that a wrapping type is not ordered from a fact about a sum
mcourteaux Sep 6, 2026
ee827e9
Add a cheap hash to Expr nodes for fast IREquality pre-checks
abadams Sep 6, 2026
bc1eaee
Pack the Expr hash into IRNode::node_type's spare bits
abadams Sep 6, 2026
3b7820c
Fix set_hash on big-endian: shift the high-quality bits into place
abadams Sep 6, 2026
566b056
Fix (U)IntImm hash discarding small values entirely
abadams Sep 7, 2026
2b43673
Tighten IRNode hash comment
abadams Sep 7, 2026
739cae9
Merge remote-tracking branch 'origin/abadams/expr_hash' into mcourtea…
mcourteaux Sep 7, 2026
ef93203
Use the Expr hash as the difference filter's summary
mcourteaux Sep 7, 2026
6169aca
Read the Expr hash directly rather than through a wrapper
mcourteaux Sep 7, 2026
9b0688c
Drop the cached hashes from KnownBound
mcourteaux Sep 7, 2026
9b6a8d1
Learn constant bounds on affine differences, peeling mul and div
mcourteaux Sep 9, 2026
ece045c
Solve scaled bounds exactly, rounding inwards
mcourteaux Sep 9, 2026
001ee6b
Don't drop an offset that overflows while peeling
mcourteaux Sep 9, 2026
281e6be
Use the learned differences as bounds on Add and Sub
mcourteaux Sep 9, 2026
cbe1c70
As the known_differences becomes an arithmetic prover, it opens the d…
mcourteaux Sep 10, 2026
a8628c3
Gate call to peel_affine_terms on node type
abadams Sep 10, 2026
618e337
Merge branch 'main' into mcourteaux/can-prove-facts
mcourteaux Sep 11, 2026
6b59abd
Merge branch 'main' into mcourteaux/can-prove-facts
mcourteaux Sep 11, 2026
e7169af
cleanup
mcourteaux Sep 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 213 additions & 1 deletion src/IRMatch.h
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,15 @@ struct WildConst {
return make_const_expr(val, type);
}

// The matched value itself, no IR built. Integer constants only.
HALIDE_ALWAYS_INLINE
int64_t bound_const_int(MatcherState &state) const noexcept {
halide_scalar_value_t val;
Type type;
state.get_bound_const(i, val, type);
return val.u.i64;
}

constexpr static bool foldable = true;

[[nodiscard]] HALIDE_ALWAYS_INLINE bool make_folded_const(halide_scalar_value_t &val, Type &ty, MatcherState &state) const noexcept {
Expand Down Expand Up @@ -490,6 +499,13 @@ struct Wild {
return state.get_binding(i);
}

// The bound node itself. Unlike make() this doesn't even touch a reference
// count, which lets predicates inspect what matched for free.
HALIDE_ALWAYS_INLINE
const BaseExprNode *bound_node(MatcherState &state) const noexcept {
return state.get_binding(i);
}

constexpr static bool foldable = false;
};

Expand Down Expand Up @@ -549,6 +565,12 @@ struct IntLiteral {
return v == b.v;
}

// The literal value itself, no IR built.
HALIDE_ALWAYS_INLINE
int64_t bound_const_int(MatcherState &state) const noexcept {
return v;
}

HALIDE_ALWAYS_INLINE
Expr make(MatcherState &state, Type type_hint) const {
return make_const(type_hint, v);
Expand Down Expand Up @@ -2554,7 +2576,7 @@ struct CanProve {
// Includes a raw call to an inlined make method, so don't inline.
[[nodiscard]] HALIDE_NEVER_INLINE bool make_folded_const(halide_scalar_value_t &val, Type &ty, MatcherState &state) const {
Expr condition = a.make(state, {});
condition = prover->mutate(condition, nullptr);
condition = prover->simplify_can_prove_condition(condition);
val.u.u64 = is_const_one(condition);
ty = Bool(condition.type().lanes());
return false;
Expand All @@ -2573,6 +2595,196 @@ std::ostream &operator<<(std::ostream &s, const CanProve<A, Prover> &op) {
return s;
}

// Like can_prove, but only looks the condition up in the facts the prover
// already knows, instead of recursively invoking it. Much cheaper, and it
// cannot recurse, so unlike can_prove it is safe in a rule whose left-hand
// side matches expressions the prover may construct while proving it.
template<typename A, typename Prover>
struct KnownTrue {
struct pattern_tag {};
A a;
Prover *prover; // An existing simplifying mutator

constexpr static uint32_t binds = bindings<A>::mask;

// This rule is a boolean-valued predicate. Bools have type UIntImm.
constexpr static IRNodeType min_node_type = IRNodeType::UIntImm;
constexpr static IRNodeType max_node_type = IRNodeType::UIntImm;
constexpr static bool canonical = true;

constexpr static bool foldable = true;

// Includes a raw call to an inlined make method, so don't inline.
[[nodiscard]] HALIDE_NEVER_INLINE bool make_folded_const(halide_scalar_value_t &val, Type &ty, MatcherState &state) const {
Expr condition = a.make(state, {});
val.u.u64 = prover->is_known_true(condition) ? 1 : 0;
ty = Bool(condition.type().lanes());
return false;
}
};

template<typename A, typename Prover>
HALIDE_ALWAYS_INLINE auto known_true(A &&a, Prover *p) noexcept -> KnownTrue<decltype(pattern_arg(a)), Prover> {
assert_is_lvalue_if_expr<A>();
return {pattern_arg(a), p};
}

template<typename A, typename Prover>
std::ostream &operator<<(std::ostream &s, const KnownTrue<A, Prover> &op) {
s << "known_true(" << op.a << ")";
return s;
}

// Detects patterns that can hand back the node they matched without building
// anything. The predicates below are restricted to these, which is what makes
// them allocation-free: it is a compile error to ask about a derived expression
// like min_diff(x, y + 1). Put the offset on the other side of the comparison
// instead: min_diff(x, y) >= 1.
template<typename A, typename = void>
struct has_bound_node : std::false_type {};

template<typename A>
struct has_bound_node<A, std::void_t<decltype(std::declval<const A &>().bound_node(std::declval<MatcherState &>()))>>
: std::true_type {};

// Bounds on the difference between two matched expressions, derived from the
// facts the prover has learned. Used as (min_diff(x, y, this) >= 0) and
// friends. When nothing is known the fold reports overflow, which the rewriter
// already treats as a failed predicate, so the rule simply doesn't fire.
template<typename A, typename B, typename Prover, bool is_min>
struct DiffBound {
struct pattern_tag {};
A a;
B b;
Prover *prover;

static_assert(has_bound_node<A>::value && has_bound_node<B>::value,
"The operands of min_diff/max_diff must be wildcards, so that "
"testing the predicate doesn't have to construct any IR.");

constexpr static uint32_t binds = bindings<A>::mask | bindings<B>::mask;

// An integer-valued term of a comparison.
constexpr static IRNodeType min_node_type = IRNodeType::IntImm;
constexpr static IRNodeType max_node_type = IRNodeType::IntImm;
constexpr static bool canonical = true;

constexpr static bool foldable = true;

[[nodiscard]] HALIDE_ALWAYS_INLINE bool make_folded_const(halide_scalar_value_t &val, Type &ty, MatcherState &state) const noexcept {
int64_t result = 0;
bool known;
if (is_min) {
known = prover->known_min_diff(a.bound_node(state), b.bound_node(state), &result);
} else {
known = prover->known_max_diff(a.bound_node(state), b.bound_node(state), &result);
}
val.u.i64 = result;
ty = Int(64);
// An unknown bound reports as overflow, failing the predicate.
return !known;
}
};

template<typename A, typename B, typename Prover>
HALIDE_ALWAYS_INLINE auto min_diff(A &&a, B &&b, Prover *p) noexcept
-> DiffBound<decltype(pattern_arg(a)), decltype(pattern_arg(b)), Prover, true> {
assert_is_lvalue_if_expr<A>();
assert_is_lvalue_if_expr<B>();
return {pattern_arg(a), pattern_arg(b), p};
}

template<typename A, typename B, typename Prover>
HALIDE_ALWAYS_INLINE auto max_diff(A &&a, B &&b, Prover *p) noexcept
-> DiffBound<decltype(pattern_arg(a)), decltype(pattern_arg(b)), Prover, false> {
assert_is_lvalue_if_expr<A>();
assert_is_lvalue_if_expr<B>();
return {pattern_arg(a), pattern_arg(b), p};
}

template<typename A, typename B, typename Prover, bool is_min>
std::ostream &operator<<(std::ostream &s, const DiffBound<A, B, Prover, is_min> &op) {
s << (is_min ? "min_diff(" : "max_diff(") << op.a << ", " << op.b << ")";
return s;
}

// As has_bound_node, for terms whose constant reads out as a plain int64_t.
template<typename A, typename = void>
struct has_bound_const_int : std::false_type {};

template<typename A>
struct has_bound_const_int<A, std::void_t<decltype(std::declval<const A &>().bound_const_int(std::declval<MatcherState &>()))>>
: std::true_type {};

// As DiffBound, but for the affine combination (ca * a - cb * b), where ca and
// cb are constants already in hand (matched WildConsts, typically) that sit
// outside a and b's own IR, so peeling can't find them. Allocation-free:
// ca/cb read as raw ints, a/b as raw bound nodes.
template<typename A, typename CA, typename B, typename CB, typename Prover, bool is_min>
struct ScaledDiffBound {
struct pattern_tag {};
A a;
CA ca;
B b;
CB cb;
Prover *prover;

static_assert(has_bound_node<A>::value && has_bound_node<B>::value,
"The a/b operands of scaled_min_diff/scaled_max_diff must be "
"wildcards, so that testing the predicate doesn't have to "
"construct any IR.");
static_assert(has_bound_const_int<CA>::value && has_bound_const_int<CB>::value,
"The coefficient operands of scaled_min_diff/scaled_max_diff "
"must be WildConsts.");

constexpr static uint32_t binds = bindings<A>::mask | bindings<CA>::mask | bindings<B>::mask | bindings<CB>::mask;

// This is an integer-valued term of a comparison.
constexpr static IRNodeType min_node_type = IRNodeType::IntImm;
constexpr static IRNodeType max_node_type = IRNodeType::IntImm;
constexpr static bool canonical = true;

constexpr static bool foldable = true;

[[nodiscard]] HALIDE_ALWAYS_INLINE bool make_folded_const(halide_scalar_value_t &val, Type &ty, MatcherState &state) const noexcept {
int64_t result = 0;
bool known;
if (is_min) {
known = prover->known_min_diff(a.bound_node(state), ca.bound_const_int(state),
b.bound_node(state), cb.bound_const_int(state), &result);
} else {
known = prover->known_max_diff(a.bound_node(state), ca.bound_const_int(state),
b.bound_node(state), cb.bound_const_int(state), &result);
}
val.u.i64 = result;
ty = Int(64);
// Report an unknown bound as an overflow, which fails the predicate.
return !known;
}
};

template<typename A, typename CA, typename B, typename CB, typename Prover>
HALIDE_ALWAYS_INLINE auto scaled_min_diff(A &&a, CA &&ca, B &&b, CB &&cb, Prover *p) noexcept
-> ScaledDiffBound<decltype(pattern_arg(a)), decltype(pattern_arg(ca)), decltype(pattern_arg(b)), decltype(pattern_arg(cb)), Prover, true> {
assert_is_lvalue_if_expr<A>();
assert_is_lvalue_if_expr<B>();
return {pattern_arg(a), pattern_arg(ca), pattern_arg(b), pattern_arg(cb), p};
}

template<typename A, typename CA, typename B, typename CB, typename Prover>
HALIDE_ALWAYS_INLINE auto scaled_max_diff(A &&a, CA &&ca, B &&b, CB &&cb, Prover *p) noexcept
-> ScaledDiffBound<decltype(pattern_arg(a)), decltype(pattern_arg(ca)), decltype(pattern_arg(b)), decltype(pattern_arg(cb)), Prover, false> {
assert_is_lvalue_if_expr<A>();
assert_is_lvalue_if_expr<B>();
return {pattern_arg(a), pattern_arg(ca), pattern_arg(b), pattern_arg(cb), p};
}

template<typename A, typename CA, typename B, typename CB, typename Prover, bool is_min>
std::ostream &operator<<(std::ostream &s, const ScaledDiffBound<A, CA, B, CB, Prover, is_min> &op) {
s << (is_min ? "scaled_min_diff(" : "scaled_max_diff(") << op.a << ", " << op.ca << ", " << op.b << ", " << op.cb << ")";
return s;
}

template<typename A>
struct IsFloat {
struct pattern_tag {};
Expand Down
5 changes: 5 additions & 0 deletions src/Lower.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,11 @@ void lower_impl(const vector<Function> &output_funcs,
s = storage_flattening(s, outputs, env, t);
log("Lowering after storage flattening:", s);

// Every pass that reads a region or an allocation size out of the IR has
// now run, so from here a clamp is only worth what its value is worth, and
// the simplifier may use what it knows to remove a redundant one.
ScopedRegionsInferred regions_inferred;

debug(1) << "Adding atomic mutex allocation...\n";
s = add_atomic_mutex(s, outputs);
log("Lowering after adding atomic mutex allocation:", s);
Expand Down
Loading
Loading