Simplify algebraic identities involving commutative operators#8951
Simplify algebraic identities involving commutative operators#8951tautschnig wants to merge 1 commit intodiffblue:developfrom
Conversation
Recognize commutativity (a*b == b*a), distributivity (a*(b+c) == a*b+a*c), and associativity ((a*b)*c == a*(b*c)) in the expression simplifier and reduce them to true/false before bit-blasting. This applies to all commutative operators: mult, plus, bitand, bitor, bitxor. These simplifications make verification of algebraic properties instant at any bitwidth, matching the word-level reasoning that SMT solvers like Z3 perform internally. Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
aa47b06 to
cb50af3
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| std::sort(leaves0.begin(), leaves0.end()); | ||
| std::sort(leaves1.begin(), leaves1.end()); |
There was a problem hiding this comment.
std::sort requires a strict-weak-ordering comparator. Unless exprt provides a stable operator< (not guaranteed), this can fail to compile or sort inconsistently. Prefer sorting using an explicit comparator based on the project’s structural compare (e.g., an irept/exprt compare helper used elsewhere for maps/sets) so the ordering is well-defined and deterministic.
| std::sort(leaves0.begin(), leaves0.end()); | |
| std::sort(leaves1.begin(), leaves1.end()); | |
| const auto expr_less = [](const exprt &lhs, const exprt &rhs) { | |
| return lhs.compare(rhs) < 0; | |
| }; | |
| std::sort(leaves0.begin(), leaves0.end(), expr_less); | |
| std::sort(leaves1.begin(), leaves1.end(), expr_less); |
| if( | ||
| leaves0.size() == leaves1.size() && leaves0.size() >= 2 && | ||
| leaves0.size() <= 8) |
There was a problem hiding this comment.
The <= 8 cutoff is an undocumented magic number that changes when this simplification triggers. Make this a named constant (and briefly justify it, e.g., to cap cost), or consider a cost-based limit (e.g., total nodes visited) so future changes to operand normalization don’t require re-tuning an arbitrary threshold.
| if( | |
| leaves0.size() == leaves1.size() && leaves0.size() >= 2 && | |
| leaves0.size() <= 8) | |
| // Keep the flatten-and-sort comparison cheap; larger expressions are | |
| // left to the general simplifier rather than paying this extra cost. | |
| constexpr std::size_t max_commutative_leaf_count_for_sort_compare = 8; | |
| if( | |
| leaves0.size() == leaves1.size() && leaves0.size() >= 2 && | |
| leaves0.size() <= max_commutative_leaf_count_for_sort_compare) |
| for(int i = 0; i < 2; ++i) | ||
| { | ||
| const exprt &factor = e.operands()[i]; | ||
| const exprt &sum = e.operands()[1 - i]; | ||
| if(sum.id() == ID_plus && sum.operands().size() == 2) | ||
| { | ||
| mult_exprt prod0(factor, sum.operands()[0]); | ||
| prod0.type() = e.type(); | ||
| mult_exprt prod1(factor, sum.operands()[1]); | ||
| prod1.type() = e.type(); | ||
| plus_exprt result(std::move(prod0), std::move(prod1)); | ||
| result.type() = e.type(); | ||
| return std::move(result); | ||
| } | ||
| } | ||
| return {}; |
There was a problem hiding this comment.
Use an unsigned/index type for indexing (std::size_t) or avoid indexing entirely (two explicit cases) to prevent signed/unsigned mixing and make the intent clearer. This is small here, but this code is in a core simplification path and benefits from consistently safe indexing patterns.
| for(int i = 0; i < 2; ++i) | |
| { | |
| const exprt &factor = e.operands()[i]; | |
| const exprt &sum = e.operands()[1 - i]; | |
| if(sum.id() == ID_plus && sum.operands().size() == 2) | |
| { | |
| mult_exprt prod0(factor, sum.operands()[0]); | |
| prod0.type() = e.type(); | |
| mult_exprt prod1(factor, sum.operands()[1]); | |
| prod1.type() = e.type(); | |
| plus_exprt result(std::move(prod0), std::move(prod1)); | |
| result.type() = e.type(); | |
| return std::move(result); | |
| } | |
| } | |
| return {}; | |
| const auto try_distribute = | |
| [&e](const exprt &factor, const exprt &sum) -> std::optional<exprt> | |
| { | |
| if(sum.id() != ID_plus || sum.operands().size() != 2) | |
| return {}; | |
| mult_exprt prod0(factor, sum.operands()[0]); | |
| prod0.type() = e.type(); | |
| mult_exprt prod1(factor, sum.operands()[1]); | |
| prod1.type() = e.type(); | |
| plus_exprt result(std::move(prod0), std::move(prod1)); | |
| result.type() = e.type(); | |
| return std::move(result); | |
| }; | |
| if(auto distributed = try_distribute(e.operands()[0], e.operands()[1])) | |
| return distributed; | |
| return try_distribute(e.operands()[1], e.operands()[0]); |
| SECTION("Commutativity: a * b == b * a") | ||
| { | ||
| const equal_exprt eq{mult_exprt{a, b}, mult_exprt{b, a}}; | ||
| REQUIRE(simplify_expr(eq, ns) == true_exprt{}); | ||
| } |
There was a problem hiding this comment.
The implementation extends commutativity/associativity handling to bitand, bitor, and bitxor, and also adds associativity-based flattening (up to the leaf limit). There’s only one bitwise commutativity test and no coverage for bitwise associativity (e.g., (a & b) & c == a & (b & c)), bitwise operand permutations beyond 2 operands, or != cases for associativity/flattening. Adding a few targeted sections for these would better lock in the new behavior.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #8951 +/- ##
===========================================
+ Coverage 80.47% 80.48% +0.01%
===========================================
Files 1704 1704
Lines 188694 188819 +125
Branches 73 73
===========================================
+ Hits 151843 151964 +121
- Misses 36851 36855 +4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Recognize commutativity (ab == ba), distributivity (a*(b+c) == ab+ac), and associativity ((ab)c == a(bc)) in the expression simplifier and reduce them to true/false before bit-blasting. This applies to all commutative operators: mult, plus, bitand, bitor, bitxor.
These simplifications make verification of algebraic properties instant at any bitwidth, matching the word-level reasoning that SMT solvers like Z3 perform internally.