How to Write a Custom LLVM Pass: A Complete Guide to the New Pass Manager
Write a custom LLVM pass by deriving from llvm::PassInfoMixin<YourPass>, implementing a run() method that returns PreservedAnalyses, and registering it via RegisterPass or the PassBuilder API to invoke it with opt -passes=your-pass.
LLVM’s transformation engine relies on modular units called passes to optimize and analyze intermediate representation (IR). Since LLVM 13, the new Pass Manager (NPM) serves as the preferred infrastructure for writing custom LLVM passes, offering fine-grained analysis caching, parallel execution capabilities, and a clean CRTP-based API. This guide demonstrates how to extend the llvm/llvm-project codebase with custom transformations using the core components defined in llvm/include/llvm/IR/PassManager.h.
Architecture of the New Pass Manager
The NPM is built around a generic, templated design that operates on specific IR units such as Function, Module, or Loop. At its core, the system uses the CRTP (Curiously Recurring Template Pattern) mix-in PassInfoMixin, which provides the run entry point and utilities for preserving analyses.
Key components include:
PassInfoMixin: The base class template located inllvm/include/llvm/IR/PassManager.hthat supplies the staticname()method and pipeline integration utilitiesAnalysisManager: Handles lazy computation, caching, and invalidation of analysis resultsPreservedAnalyses: A return type that communicates which analyses remain valid after a transformation executes
Essential Source Files
| Component | Purpose | Key Source File |
|---|---|---|
| Pass base class | CRTP mix-in providing run entry point and preservation utilities |
llvm/include/llvm/IR/PassManager.h |
| Pass registration | Global registry for textual pipeline discovery | llvm/include/llvm/PassRegistry.h |
| Pass manager implementation | Run-logic, analysis caching, and invalidation semantics | llvm/lib/IR/PassManager.cpp |
| Driver implementation | opt tool integration for the NPM |
llvm/tools/opt/NewPMDriver.cpp |
Implementing a Function-Level Transformation Pass
To write a custom LLVM pass that transforms IR, create a class inheriting from PassInfoMixin<YourClass>. The pass operates on a specific IR unit and must implement a run method that accepts the unit and an AnalysisManager.
Minimal Pass Implementation
#include "llvm/IR/PassManager.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/InstIterator.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;
struct MyFunctionPass : PassInfoMixin<MyFunctionPass> {
PreservedAnalyses run(Function &F, FunctionAnalysisManager &) {
// Example transformation: print every instruction.
for (Instruction &I : instructions(F)) {
outs() << "Inst: " << I << "\n";
}
// We did not modify the IR → preserve *all* analyses.
return PreservedAnalyses::all();
}
// Optional – used by the textual pipeline parser.
static StringRef name() { return "my-func-pass"; }
};
Key implementation details:
- CRTP inheritance:
PassInfoMixin<MyFunctionPass>provides the boilerplate for pipeline integration and the staticname()method - Return semantics:
PreservedAnalyses::all()tells the manager that no analyses were invalidated, allowing subsequent passes to reuse cached results; usePreservedAnalyses::none()orPA.abandon<AnalysisType>()when transformations invalidate specific analyses - Static name method: Enables invocation via
opt -passes=my-func-pass
Registering and Executing Your Pass
Command-Line Registration
Register your pass using the RegisterPass template to make it discoverable by the opt driver:
static RegisterPass<MyFunctionPass>
X("my-func-pass", "Demo pass that prints each instruction",
false, false); // (isCFGOnly, isAnalysis)
The boolean parameters indicate whether the pass is CFG-only and whether it is an analysis pass. This registration interfaces with the global pass registry defined in llvm/include/llvm/PassRegistry.h, which both the legacy and new opt drivers consult.
Programmatic Pipeline Construction
For tools that embed LLVM, construct and execute pipelines using PassBuilder and the adaptor utilities:
#include "llvm/Passes/PassBuilder.h"
PassBuilder PB; // Central builder for the NPM
FunctionPassManager FPM;
FPM.addPass(MyFunctionPass()); // Insert our pass
ModulePassManager MPM;
MPM.addPass(createModuleToFunctionPassAdaptor(std::move(FPM)));
// Execute on the module.
PB.runPipeline(*M, MPM);
The createModuleToFunctionPassAdaptor wrapper, defined near line 66 in llvm/include/llvm/IR/PassManager.h, enables a FunctionPassManager to run over each function in a module. You can also register custom pipeline names via PassBuilder::registerPipelineParsingCallback to expose them to the -passes= command-line interface.
Writing Custom Analyses
Analysis passes compute information that transformation passes consume. They follow the same CRTP pattern but return a result object rather than PreservedAnalyses:
struct MyFunctionAnalysis : PassInfoMixin<MyFunctionAnalysis> {
struct Result { unsigned NumInsts = 0; };
Result run(Function &F, FunctionAnalysisManager &) {
Result R;
for (auto &I : instructions(F))
++R.NumInsts;
return R;
}
static StringRef name() { return "my-func-analysis"; }
};
static RegisterPass<MyFunctionAnalysis>
Y("my-func-analysis", "Counts instructions in a function", true, false);
A transformation pass can then obtain this result via:
auto &A = getAnalysis<MyFunctionAnalysis>(F);
outs() << "Function has " << A.NumInsts << " instructions\n";
Analyses are cached per IR unit and automatically recomputed only when a transformation pass explicitly abandons them via PreservedAnalyses::abandon<AnalysisType>().
Summary
- Derive custom passes from
PassInfoMixin<YourPass>using CRTP inheritance; useRequiredPassInfoMixinfor passes that must never be skipped - Implement
PreservedAnalyses run(IRUnit &, AnalysisManager &)to control which analyses remain valid after execution - Register passes via
RegisterPass<YourPass>inllvm/include/llvm/PassRegistry.hto expose them to theoptdriver - Construct pipelines programmatically using
PassBuilderand adaptors likecreateModuleToFunctionPassAdaptordefined inllvm/include/llvm/IR/PassManager.h - Return
PreservedAnalyses::all()for read-only passes,none()for destructive transformations, or usepreserve<AnalysisType>()for selective invalidation
Frequently Asked Questions
What is the difference between the legacy Pass Manager and the new Pass Manager?
The legacy Pass Manager relied on explicit pass dependencies and lacked fine-grained analysis caching. The new Pass Manager, default since LLVM 13, provides a fully generic, templated API with automatic analysis preservation, parallel execution support, and unified pipeline construction through PassBuilder as implemented in llvm/lib/IR/PassManager.cpp.
Can I use the same pass implementation for both LLVM IR and MLIR?
While the architectural concepts are identical, the implementations are distinct. LLVM IR passes use PassInfoMixin from llvm/include/llvm/IR/PassManager.h, whereas MLIR passes inherit from OperationPass defined in mlir/include/mlir/Pass/Pass.h. Both systems share design principles detailed in mlir/docs/PassManagement.md.
How do I prevent my pass from being skipped by the optimizer?
Derive your pass from llvm::RequiredPassInfoMixin<YourPass> instead of PassInfoMixin. This signals to the pass manager that the pass must always execute regardless of optimization level or previous transformations.
When should I use PreservedAnalyses::none() versus all()?
Return PreservedAnalyses::all() only when your pass guarantees it did not modify the IR unit, allowing the manager to reuse cached analyses for subsequent passes. Return PreservedAnalyses::none() when your transformation invalidates all analysis results, forcing recomputation for any dependent passes in the pipeline.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →