What Are LLVM Passes? Architecture, Types, and Implementation Guide

LLVM passes are modular units of transformation or analysis that operate on LLVM IR, serving as the building blocks of the compiler’s optimization pipeline and managed by pass managers that schedule, run, and coordinate their execution.

LLVM passes form the core infrastructure of the LLVM compiler framework, enabling developers to implement optimizations and code analysis as discrete, composable components. Within the llvm/llvm-project repository, these passes operate on LLVM Intermediate Representation (IR) to either analyze code properties or transform the IR to improve performance. Understanding how LLVM passes work is essential for anyone extending the compiler toolchain or writing custom optimization routines.

Core Architecture of LLVM Passes

All LLVM passes inherit from a common base class and are categorized by the granularity of IR they manipulate. The architecture distinguishes between infrastructure for pass management, metadata registration, and the execution model.

The Pass Base Class and Hierarchy

Every LLVM pass inherits from llvm::Pass, defined in llvm/include/llvm/Pass.h. This base class establishes the common interface including virtual methods such as runOnModule, runOnFunction, getPassName, and print. It also handles bookkeeping like the unique pass ID used to identify passes in the pipeline.

Pass metadata is stored in the PassInfo class, located in llvm/include/llvm/PassInfo.h. The RegisterPass<> macro creates instances of PassInfo to register a pass with the global PassRegistry, defining its command-line argument, description, and whether it is an analysis pass.

Pass Kinds and IR Granularity

The PassKind enum distinguishes the IR granularity a pass operates on:

  • PT_Module: Operates on the entire module (ModulePass)
  • PT_Function: Operates on individual functions (FunctionPass)
  • PT_Loop: Operates on loop nests (LoopPass)
  • PT_CallGraphSCC: Operates on strongly connected components of the call graph

Legacy vs. New Pass Manager

LLVM maintains two distinct pass management infrastructures:

Legacy Pass Manager: The original infrastructure using legacy::PassManager and legacy::FunctionPassManager. Passes inherit from llvm::Pass and override virtual methods like runOnFunction. This system remains widely used by tools like opt.

New Pass Manager (PM): Introduced in LLVM 10, located in llvm/include/llvm/IR/PassManager.h. This infrastructure is type-safe and uses C++ concepts rather than virtual inheritance. Any class implementing a compatible run(IR, AnalysisManager...) method can function as a pass, eliminating the need to inherit from a specific base class.

Types of LLVM Passes: Analysis and Transformation

LLVM passes fall into two fundamental categories based on their interaction with the IR.

Analysis passes compute information about the IR without modifying it. For example, AliasAnalysis determines which memory locations might alias. These passes return results that subsequent passes query, and the analysis manager caches these results to avoid recomputation.

Transformation passes modify the LLVM IR to optimize or canonicalize it. Examples include InstCombine (instruction combining) and SimplifyCFG. After a transformation runs, it returns a PreservedAnalyses object to indicate which cached analysis results remain valid. If a transformation modifies IR that an analysis depends on, that analysis is automatically invalidated and removed from the cache.

How LLVM Passes Work in the Optimization Pipeline

The execution of LLVM passes follows a structured lifecycle managed by pass managers and coordinated through the PassBuilder infrastructure.

Pipeline Construction with PassBuilder

The PassBuilder class, defined in llvm/include/llvm/Passes/PassBuilder.h, constructs optimization pipelines through helper methods like buildModuleOptimizationPipeline and buildFunctionSimplificationPipeline. It creates a hierarchy of pass managers: ModulePassManager owns FunctionPassManager instances, which in turn may own LoopPassManager instances.

Scheduling and Execution

Each pass manager contains a vector of pass concepts (PassConceptT). When a manager’s run method is invoked, it iterates over its passes, invoking each pass’s run method with the appropriate IR unit and analysis manager.

Analysis Caching and Invalidation

After each pass executes, the infrastructure updates the PreservedAnalyses set. This set tracks which analyses remain valid after the transformation. If a pass returns PreservedAnalyses::none(), all cached analyses are invalidated. If it returns PreservedAnalyses::all(), all analyses remain valid. This mechanism ensures that subsequent passes receive up-to-date analysis results without manual cache management.

Writing Your First LLVM Pass

The following examples demonstrate how to implement passes using both the legacy and new pass manager infrastructures.

Example 1: A Legacy Function Pass (Instruction Counter)

This pass counts instructions in each function without modifying the IR. It inherits from FunctionPass and uses the registration macro.

// File: MyInstrCount.cpp
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"

using namespace llvm;

namespace {
struct InstrCountPass : public FunctionPass {
  static char ID;
  InstrCountPass() : FunctionPass(ID) {}

  bool runOnFunction(Function &F) override {
    unsigned Count = 0;
    for (auto &BB : F)
      for (auto &I : BB)
        ++Count;
    errs() << "Function " << F.getName() << " has " << Count << " instructions.\n";
    return false;                     // does not modify IR
  }

  StringRef getPassName() const override {
    return "Instruction Count Pass";
  }
};
} // namespace

char InstrCountPass::ID = 0;
static RegisterPass<InstrCountPass>
    X("instr-count", "Counts instructions in each function", false, false);

Build and run the pass using the legacy opt tool:

clang++ -fno-exceptions -fno-rtti \
  `llvm-config --cxxflags --ldflags --system-libs --libs core` \
  MyInstrCount.cpp -o MyInstrCount.so
opt -load ./MyInstrCount.so -instr-count my.bc -o /dev/null

Example 2: A New Pass Manager Function Pass

This example prints basic block names using the modern infrastructure. Note that no inheritance is required—only a compatible run method signature.

// File: PrintBBNames.cpp
#include "llvm/IR/PassManager.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"

using namespace llvm;

struct PrintBBNamesPass {
  PreservedAnalyses run(Function &F, FunctionAnalysisManager &) {
    for (auto &BB : F)
      errs() << "BasicBlock: " << BB.getName() << "\n";
    return PreservedAnalyses::all();   // analysis results remain valid
  }

  static StringRef name() { return "PrintBBNamesPass"; }
};

int main(int argc, char **argv) {
  LLVMContext C;
  SMDiagnostic Err;
  std::unique_ptr<Module> M = parseIRFile(argv[1], Err, C);
  if (!M) return 1;

  FunctionAnalysisManager FAM;
  PassBuilder PB;
  PB.registerFunctionAnalyses(FAM);

  FunctionPassManager FPM;
  FPM.addPass(PrintBBNamesPass{});
  for (Function &F : *M)
    FPM.run(F, FAM);
}

Example 3: Integrating Custom Passes into Default Pipelines

Insert a custom transformation into a standard optimization pipeline using PassBuilder and adaptors:

#include "llvm/Passes/PassBuilder.h"
#include "llvm/IR/PassManager.h"

struct MyCustomPass {
  PreservedAnalyses run(Function &F, FunctionAnalysisManager &) {
    // …your transformation logic…
    return PreservedAnalyses::none();   // all analyses must be recomputed
  }
};

int main() {
  PassBuilder PB;
  ModulePassManager MPM = PB.buildPerModuleDefaultPipeline(OptimizationLevel::O2);
  
  // Insert the custom pass using a module-to-function adaptor
  MPM.addPass(createModuleToFunctionPassAdaptor(
        MyCustomPass{}, /*EagerlyInvalidate=*/false));
  // …run MPM on a Module…
}

Key Source Files for LLVM Pass Development

Understanding these header files is essential for developing passes within the llvm/llvm-project codebase:

Summary

  • LLVM passes are modular components that analyze or transform LLVM IR within the compiler pipeline.
  • Two categories exist: analysis passes (read-only, cached) and transformation passes (modify IR, trigger invalidation).
  • Two infrastructures coexist: the legacy pass manager (virtual inheritance-based) and the new pass manager (type-safe, concept-based, LLVM 10+).
  • PassBuilder constructs optimization pipelines, while pass managers schedule execution and handle the PreservedAnalyses protocol to manage cache validity.
  • Key files include Pass.h for legacy bases, PassManager.h for modern infrastructure, and PassBuilder.h for pipeline construction.

Frequently Asked Questions

What is the difference between the legacy and new pass managers in LLVM?

The legacy pass manager requires passes to inherit from llvm::Pass and override virtual methods like runOnFunction. It uses a polymorphic approach with deep inheritance hierarchies. The new pass manager, available since LLVM 10, uses C++ templates and concepts—passes only need to implement a run() method with the correct signature, eliminating virtual dispatch overhead and enabling better compile-time type checking.

How do I register a custom LLVM pass with the opt tool?

For the legacy pass manager, include the RegisterPass<> macro from llvm/PassInfo.h in your source file: static RegisterPass<MyPass> X("pass-id", "Description", false, false); Then compile as a shared library and load with opt -load ./MyPass.so -pass-id. For the new pass manager, registration typically occurs in your driver code by adding the pass to a PassManager instance, or by extending the PassBuilder callback mechanism for plugin registration.

What is the difference between analysis passes and transformation passes?

Analysis passes examine the IR to compute information (such as dominator trees or alias analysis) without modifying it. They return analysis results that other passes consume. Transformation passes modify the IR to improve performance or canonicalize code, such as constant folding or dead code elimination. Transformation passes must specify which analyses they preserve using the PreservedAnalyses API so the analysis manager knows which cached results remain valid.

When should I invalidate analyses in a transformation pass?

You should return PreservedAnalyses::none() when your transformation modifies IR in ways that could invalidate any analysis results, forcing recomputation for subsequent passes. Return PreservedAnalyses::all() only if the pass makes no changes or changes that cannot affect any analysis (rare). For granular control, use PreservedAnalyses::set<SpecificAnalysis>() to indicate exactly which analyses remain valid, allowing the infrastructure to preserve cache entries for unrelated analyses.

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 →