Pass pipeline and analyses
The verifier decides whether a pass is correct; invalidation decides what later passes see; diagnostics decide whether failure is legible.
A pass is an operation-level transform
struct CountLoads : PassWrapper<CountLoads, OperationPass<func::FuncOp>> {
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(CountLoads)
StringRef getArgument() const final { return "count-loads"; }
void runOnOperation() override {
unsigned n = 0;
getOperation().walk([&](memref::LoadOp) { ++n; });
// ...
}
};
OperationPass<func::FuncOp> restricts the pass to one op type, and the pass manager enforces the restriction when it schedules the pass. getDependentDialects declares dialects the pass may create, because the context is loaded and then locked before a multithreaded pipeline starts.
The pipeline is text
--pass-pipeline='builtin.module(func.func(canonicalize,count-loads))' places each pass under an anchor, and the nesting has to match each pass’s restriction. mlir-opt --help lists the names your build registered; --mlir-print-ir-after-all dumps the IR between passes, which is the cheapest way to see what a pass actually did.
Analyses are cached and invalidated
The AnalysisManager computes analyses lazily per operation and caches them; getAnalysis<LoopDepth>() returns the cached instance. Every pass invalidates every analysis by default. A pass that changed nothing calls markAllAnalysesPreserved(); one that preserved specific analyses calls markAnalysesPreserved<T>(). Forgetting both is safe and merely slow. The unsafe direction is claiming a preservation you did not achieve, which hands stale data to the next pass.
Diagnostics decide correctness
A pass that cannot continue emits a diagnostic at a specific op and then signals failure:
getOperation().emitOpError("expected a static shape, got ") << type;
return signalPassFailure();
The diagnostic carries the location, and the signal stops the pipeline so that PassManager::run returns failure. Without the signal, later passes run on IR the pass already called broken. Without the diagnostic, the failure has a pass name but no operation.
Things that break
A pass restricted to func.func and listed directly under builtin.module is rejected at scheduling time with an error naming the restriction. A pass that mutates IR while claiming all analyses preserved gives the next pass stale dominance information; the symptom is a crash in a later pass that disappears under --mlir-disable-threading. That difference between threaded and single-threaded runs is the tell.
Sources
- Pass Infrastructuredocs · LLVM 19
- Diagnostic Infrastructuredocs
- MLIR: A Compiler Infrastructure for the End of Moore's Lawpaper
A version records the documentation the claim was checked against, not a house pin. Confirm flags with --help on the release you installed.
Check yourself
You run `mlir-opt --pass-pipeline='builtin.module(my-func-pass)'` on a pass declared as `OperationPass<func::FuncOp>`. What happens?
A pass erases an operation and does not call any preservation API. What must the pass manager assume about cached analyses?
A pass calls `signalPassFailure()` but emits no diagnostic. What does the run look like?