Skip to content

Upstream simplifier rule verification tool - #9334

Merged
alexreinking merged 10 commits into
alexreinking/run-process-redirectfrom
abadams/simplifier_verifier
Aug 16, 2026
Merged

Upstream simplifier rule verification tool#9334
alexreinking merged 10 commits into
alexreinking/run-process-redirectfrom
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

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 70.09%. Comparing base (27561c3) to head (97dc27a).

Additional details and impacted files
@@                          Coverage Diff                          @@
##           alexreinking/run-process-redirect    #9334      +/-   ##
=====================================================================
+ Coverage                              70.04%   70.09%   +0.04%     
=====================================================================
  Files                                    258      258              
  Lines                                  78716    78717       +1     
  Branches                               19163    19163              
=====================================================================
+ Hits                                   55140    55180      +40     
+ Misses                                 17878    17867      -11     
+ Partials                                5698     5670      -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)

@alexreinking

Copy link
Copy Markdown
Member

Needs a tweak for Windows:

[639/712] Building CXX object simplifier_rule_verifier\CMakeFiles\rule_verifier_support.dir\z3.cpp.obj
FAILED: simplifier_rule_verifier/CMakeFiles/rule_verifier_support.dir/z3.cpp.obj 
C:\PROGRA~1\MICROS~2\2022\COMMUN~1\VC\Tools\MSVC\1444~1.352\bin\Hostx64\x64\cl.exe  /nologo /TP -DHALIDE_ENABLE_RTTI -DHALIDE_VERSION_MAJOR=22 -DHALIDE_VERSION_MINOR=0 -DHALIDE_VERSION_PATCH=0 -DHALIDE_WITH_EXCEPTIONS -IC:\build_bot\worker\halide-llvm_main-x86-64-windows\halide-source\apps\simplifier_rule_verifier -external:IC:\build_bot\worker\halide-llvm_main-x86-64-windows\halide-install\include -external:IC:\build_bot\worker\halide-llvm_main-x86-64-windows\halide-install\share\tools -external:W0 /DWIN32 /D_WINDOWS /EHsc /O2 /Ob1 /DNDEBUG -std:c++17 -MD -Zi /showIncludes /Fosimplifier_rule_verifier\CMakeFiles\rule_verifier_support.dir\z3.cpp.obj /Fdsimplifier_rule_verifier\CMakeFiles\rule_verifier_support.dir\rule_verifier_support.pdb /FS -c C:\build_bot\worker\halide-llvm_main-x86-64-windows\halide-source\apps\simplifier_rule_verifier\z3.cpp
C:\build_bot\worker\halide-llvm_main-x86-64-windows\halide-source\apps\simplifier_rule_verifier\z3.cpp(537): error C3861: 'popen': identifier not found
C:\build_bot\worker\halide-llvm_main-x86-64-windows\halide-source\apps\simplifier_rule_verifier\z3.cpp(537): error C3861: 'pclose': identifier not found

Comment thread apps/simplifier_rule_verifier/z3.cpp Outdated
@alexreinking
alexreinking changed the base branch from main to alexreinking/run-process-redirect August 15, 2026 18:07
@alexreinking
alexreinking force-pushed the abadams/simplifier_verifier branch from b186ccf to 90be1b4 Compare August 15, 2026 18:07
abadams and others added 10 commits August 15, 2026 14:08
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>
popen and pclose are POSIX-only, so the Windows bots couldn't build the app.
The pipe was never read: z3's output is redirected to a file and read back
from there, and only the exit status is used. system() gives us that and is
standard C++, so there's nothing to shim.

Also quote the two temporary file paths, which on Windows land under a temp
directory that may contain spaces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Util.h has run_process, which spawns the child directly instead of going
through a shell, and the merged branch adds an overload that redirects its
stdout to a file. That's exactly what this needs, so no shell command has to
be built at all: the temporary file paths become plain arguments, and the
quoting added for Windows temp directories is no longer necessary.

The return value is now the child's exit code rather than a shell wait
status, so the diagnostic for a failed query reports a usable number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Halide's printer emits small floats as e.g. 1.000000e-16f, but the parser
stopped at the 'e' and then failed on the leftover exponent. Found by feeding
it a corpus of Exprs collected from a real build, where 16 of the 844 were
unparseable for this reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The CEGIS interpreter only represents Int(32) and Bool leaves, and asserted
on anything else. Feeding it a corpus of Exprs collected from a real build,
most of which mention uint32, uint64 or float variables, that assert killed
the process rather than reporting that there was nothing to search.

Check the leaf types up front and return an undefined Expr, which is what
callers already expect when no equivalent is found.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@alexreinking
alexreinking force-pushed the abadams/simplifier_verifier branch from 90be1b4 to 97dc27a Compare August 15, 2026 18:08

@alexreinking alexreinking left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I stacked this on top of the run_process change

@alexreinking
alexreinking merged commit bcfb755 into main Aug 16, 2026
28 checks passed
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