Skip to content

Upstream simplifier rule verification tool - #9334

Open
abadams wants to merge 6 commits into
mainfrom
abadams/simplifier_verifier
Open

Upstream simplifier rule verification tool#9334
abadams wants to merge 6 commits into
mainfrom
abadams/simplifier_verifier

Conversation

@abadams

@abadams abadams commented Aug 13, 2026

Copy link
Copy Markdown
Member

We formally verify our simplifier rules, but the tool for doing this was stashed in an old out-of-date branch around 5 years old. I had claude bring it up to date and recheck all the rules. This PR puts it in main as an app and tests it in CI. The existing rules all verified except for one that was only correct due to some surrounding context. It's better if the rules are correct without needing context, so I had it add a term to the predicate.

Lots of code, but almost all of it is confined to an app.

abadams and others added 6 commits August 13, 2026 10:53
A port of apps/super_simplify from the super_simplify_v3 branch, where it
had been left to bit-rot since 2021. filter_rewrite_rules takes a file of
proposed simplifier rules in Simplify_*.cpp syntax and checks each one with
z3 for correctness, against a reduction order for termination, and against
the other rules for subsumption. super_simplify searches for the smallest
expression equivalent to a given one by CEGIS.

Dropped synthesize_predicate.cpp and the tools that depended on it. Both of
its entry points were only reachable from disabled branches, and it was
built on internal Simplify APIs that would have forced the app to build
against src/ rather than an installed Halide. The predicate synthesis that
filter_rewrite_rules actually uses is implemented inline and is unaffected.

Also fixed, in the course of getting it running again:

- z3 no longer tags models with "model", so every counterexample came back
  empty.
- fold() in a rule aborted the SMT conversion.
- Rules z3 disproved were still emitted as good rules.
- bvumod is not an SMT-LIB operator, so unsigned mod produced invalid SMT2.
- Integer division and modulo are Euclidean at every width in Halide, but
  the narrow-integer encoding used raw bvsmod and a floor-division bvsdiv,
  and handled neither division by zero. Verified against div_imp/mod_imp on
  all 65536 int8 operand pairs.
- An unmodelled intrinsic in one rule aborted the whole run.
- The parser had no unary minus, and could not reparse a Select as boolean.

The parser's precedence ladder is now a precedence-climbing loop over a
table of operators, which drops the pushback stack it used to thread
between levels. Verified by reparsing every rule in src/Simplify_*.cpp and
diffing: the only change is that && and || are left-associative, as in C++.

Tested by ctest under the label simplifier_rule_verifier, and by make test.
Both skip when z3 isn't installed, so the macOS CI job that builds apps now
installs it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A remainder always lies in [0, |c1|), so for c2 outside that range the left
hand side is false while the right hand side need not be. The rule relied on
the bounds analysis in Simplify_EQ having already folded such comparisons
away, which it does, so this changes no behaviour. It makes the rule stand on
its own rather than on the order of the passes around it.

Found by apps/simplifier_rule_verifier, which checks rules in isolation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All but one predate the port.

The search for a free wildcard name, used when demoting a constant wildcard
that appears in neither a fold nor a predicate, tested against a copy of the
rule taken before any substitution. Every constant wildcard therefore picked
the same name and they collapsed into one variable, so the rule written out
was not the rule that had been checked. Given

  rewrite(select(u, x + c0, x + c1), x + select(u, c0, c1))

it emitted select(x, y + z, z + y) -> select(x, y, y) + z, which is false.

Parsing a bit-vector out of a z3 model shifted by up to 64 to find the sign
bit, which is undefined and in practice took the sign-extend branch every
time, so any counterexample mentioning a variable of 32 bits or wider was
reported wrong. It also built every binding as an Int(32) regardless of the
variable's real type. Both now come from the types the query was built with,
which also lets z3's names for let-bound subexpressions be filtered by lookup
rather than by guessing that anything starting with 't' is one.

Variables of type Int(64) were declared as bit-vectors while their operators
were emitted as unbounded-Int ones, so z3 rejected the query and the rule was
silently classed unverifiable.

likely and likely_if_innermost were not understood by the SMT conversion, so
rules mentioning them were emitted without ever being checked. They only
carry a branch hint, so the conversion now sees through them, as it does for
fold. The parser was also turning likely_if_innermost into likely, quietly
changing such rules on round-trip.

all_possible_exprs_that_compute_associative_op_helper was missing the bail-out
that its sibling has, so a long enough chain of terms would shift past the
width of an int and exhaust memory well before that.

consume() read a byte before checking it was in bounds.

The binding map threaded through predicate synthesis was left over from the
dropped synthesize_predicate, and was shadowed by the inner declarations that
replaced it, so the code substituting it back into the rule did nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Linux buildbots already build and test every app, so the rule verifier
gets its coverage there. Installing z3 on the macOS runner only added load to
a machine that's already oversubscribed, for a second set of the same results.

Without z3 the tests aren't registered at all, so the macOS apps build is
unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
satisfy() has to see through likely and likely_if_innermost, since they don't
change the value and z3 has no notion of them. IROperator.h already has a
helper for exactly that, so use it on the whole expression instead of
special-casing the two intrinsics in the SMT conversion's Call visitor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The original app used the ThreadPool in src/, which isn't part of the public
API, so the port replaced it with a small index-based work queue. There's an
equivalent in tools/halide_thread_pool.h with the same async() interface,
which apps can reach through Halide::Tools, so use that instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@abadams

abadams commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

TODO: Check this doesn't take too long in CI

@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.97%. Comparing base (52bb3e0) to head (8cb9cf4).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9334      +/-   ##
==========================================
- Coverage   70.02%   69.97%   -0.06%     
==========================================
  Files         258      258              
  Lines       78214    78240      +26     
  Branches    19038    19047       +9     
==========================================
- Hits        54773    54748      -25     
- Misses      17786    17809      +23     
- Partials     5655     5683      +28     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@alexreinking

Copy link
Copy Markdown
Member

TODO: Check this doesn't take too long in CI

Looks fine to me:

simplifier_rule_verifier    =   8.49 sec*proc (6 tests)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants