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 in llvm/include/llvm/IR/PassManager.h that supplies the static name() method and pipeline integration utilities
  • AnalysisManager: Handles lazy computation, caching, and invalidation of analysis results
  • PreservedAnalyses: 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 static name() method
  • Return semantics: PreservedAnalyses::all() tells the manager that no analyses were invalidated, allowing subsequent passes to reuse cached results; use PreservedAnalyses::none() or PA.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; use RequiredPassInfoMixin for passes that must never be skipped
  • Implement PreservedAnalyses run(IRUnit &, AnalysisManager &) to control which analyses remain valid after execution
  • Register passes via RegisterPass<YourPass> in llvm/include/llvm/PassRegistry.h to expose them to the opt driver
  • Construct pipelines programmatically using PassBuilder and adaptors like createModuleToFunctionPassAdaptor defined in llvm/include/llvm/IR/PassManager.h
  • Return PreservedAnalyses::all() for read-only passes, none() for destructive transformations, or use preserve<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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →