[mlir] Avoid rewriting unreachable blocks in the greedy driver (#225814)
A rewrite can disconnect a block after the iteration's initial CFG sweep.
Operations queued in that block can then develop self-referential SSA uses,
which are permitted in unreachable code but can cause folds or patterns to
crash or repeatedly re-enqueue operations. Worklist processing may never
return, preventing the next unreachable-block sweep from running. The
constant-branch-folding reproducer in #221152 demonstrates this failure.
Track reachability through rewriter notifications and skip worklist
operations in unreachable blocks until the next iteration removes them.
This extends #153957, which fixed the unreachable `arith.addi`
canonicalization hang in #153732 by erasing unreachable blocks before
applying patterns and deferred handling blocks that become unreachable
during rewriting. #154038 similarly protects the walk-based rewrite driver
by skipping unreachable blocks instead of erasing them. The reachable
graph-region self-cycle crash in #194824 and #205064 is a separate
problem; #207185 proposes a fold-specific fix for it.
A forwarding `ReachabilityListener` sits between the rewriter and the
worklist driver. Notifications only record what changed: terminator
changes and blocks that leave a region are recorded per block. Queries happen
between rewrites, once per popped operation, and bring the cache up to
date. A region is traversed from its entry block when it has no cached
state or when its entry block changed. For recorded terminator changes,
the cached successors of each block are compared with its current
successors.
Added edges extend the reachable set. A lost edge whose target is gone
or has no predecessors left, and whose target's cached successors are all
still successors of the changed block, only drops the target; this is the
shape left by folding a constant conditional branch and by merging a block
into its predecessor. Any other lost edge marks the blocks behind its target
uncertain, and only a query for one of them traverses the region.
In-place folds are reported to the listener by the driver. Nested regions check
their containing blocks up to the scope. Caches are cleared at worklist-pass
boundaries. With `MLIR_ENABLE_EXPENSIVE_PATTERN_API_CHECKS`,
every query is checked against a fresh traversal.
Tests cover inserted and redirected blocks, blocks connected before
rewrite completion, nested regions, block moves and merges, entry-block
insertion and erasure, edges lost by two blocks in one rewrite, a lost edge into a
loop, a block reconnected after being disconnected, in-place folds that
retarget a branch, and explicit op lists. Observer diagnostics check
whether affected operations are processed within a single greedy iteration.
Skipped operations are not automatically retried if a later rewrite makes their
block reachable; this is documented and tested in each strictness mode.
Compile-time impact of `mlir-opt -canonicalize` on synthetic inputs,
same Release build with assertions, min / median wall time in seconds over 3
runs. Outputs are identical with and without this patch. The inputs
behind a loop header cannot merge into the entry block, so every query goes
through the cache; invalidating the cache on block insertion and
erasure, or traversing after every lost edge, made them quadratic (up to 50x
slower).
| Input | Baseline | This patch |
|---|---|---|
| 1 block, 80k foldable arith ops | 0.135 / 0.139 | 0.144 / 0.145 |
| 20k foldable ops in 30-deep nested `scf.if` | 0.079 / 0.080 | 0.078 /
0.079 |
| 20k-block chain of foldable ops behind a loop header, no CFG change |
0.410 / 0.414 | 0.416 / 0.420 |
| 20k functions, each a 4-block CFG with one branch | 0.565 / 0.575 |
0.623 / 0.633 |
| 20k-block chain of constant `cf.cond_br` | 0.151 / 0.158 | 0.158 /
0.159 |
| 20k-block chain of diamonds whose pass-through blocks fold | 0.145 /
0.151 | 0.164 / 0.165 |
| Same diamonds behind a loop header | 0.145 / 0.146 | 0.172 / 0.177 |
| 20k blocks behind a loop header, identical-successor `cf.cond_br` fold
and merge | 0.232 / 0.237 | 0.253 / 0.256 |
| 20k constant `cf.cond_br` into a shared dead block, behind a loop
header | 1.571 / 1.578 | 1.622 / 1.626 |
Assisted-by: Codex
Assisted-by: Claude Code