Designing a Minimal LLVM New Pass Manager Pass: Requirements, Boundaries, and Checks
A concise guide to creating a minimal LLVM New Pass Manager pass, covering requirements, the smallest workable design, trust boundaries, operational checks, failure modes, and verification steps.
21 Sept 2026, 08:22 UTC

Requirements
To use LLVM's New Pass Manager (NPM) you need:
- LLVM IR as the input representation.
- A pass class that inherits from
PassInfoMixinand implements arunmethod returningPreservedAnalyses. - A pipeline builder (
PassBuilder) that registers the pass via a lambda or named pipeline.
Smallest Suitable Design
The minimal NPM pass is a single FunctionPass that does not depend on any external analysis. It registers itself with the PassBuilder using a simple lambda:
#include "llvm/IR/PassManager.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;
struct TestPass : PassInfoMixin {
PreservedAnalyses run(Function &F, FunctionAnalysisManager &) {
errs() << "TestPass saw function: " << F.getName() << '\n';
// This pass does not modify the IR, so it preserves everything.
return PreservedAnalyses::all();
}
};
static void registerTestPass(PassBuilder &PB) {
PB.registerPipelineParsingCallback(
[](StringRef Name, FunctionPassManager &FPM,
ArrayRef) {
if (Name == "test-pass") {
FPM.addPass(TestPass());
return true;
}
return false;
});
}
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() {
return {LLVM_PLUGIN_API_VERSION, "test-pass", LLVM_VERSION_STRING,
[](PassBuilder &PB) { registerTestPass(PB); }};
}
Compile this as a shared library (-fPIC -shared) and load it with opt -load-pass-plugin=<libtestpass.so> -passes='test-pass'.
Trust and Data Boundaries
The pass receives only the Function reference and a FunctionAnalysisManager. It must:
- Never assume ownership of memory outside the IR; all modifications must go through LLVM IR APIs (e.g.,
IRBuilder,Value::replaceAllUsesWith). - Not retain pointers to IR objects after
runreturns, unless those objects are preserved by the returnedPreservedAnalyses. - Avoid static or global state that could cause cross-invocation interference.
Operational Checks
LLVM runs the IR verifier before and after each pass. If a pass claims to preserve an analysis but actually invalidates it, the verifier will detect inconsistencies and LLVM will call report_fatal_error. The PassManager also checks that the PreservedAnalyses returned by a pass matches what the next pass expects.
Failure Modes and Design Triggers
- Incorrect analysis preservation: Returning
PreservedAnalyses::all()while modifying the IR leads to stale analysis results and possible miscompiles. - Version sensitivity: The NPM API changed between LLVM 11 and LLVM 14. Code that uses the legacy
PassManageror oldRegisterPassmacros may fail to compile unless guarded by#ifdef LLVM_ENABLE_NEWPM. - Design change triggers: Switching back to the legacy PassManager, adding a new analysis interface, or altering the pass granularity (e.g., moving to a ModulePass) requires updating the pipeline registration and adjusting analysis dependencies.
Practical Example: Building and Testing the Pass
Run the following in a shell where you have write permission to the working directory. LLVM 14 or newer is assumed.
- Build LLVM (version 14 or later) or install a pre-built package that includes headers and libraries.
- Compile the pass, assuming the source is in
TestPass.cpp:clang++ -fPIC -shared TestPass.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core irreader` -o libTestPass.soThis produces a shared library in the current directory.
- Create a simple IR file (
sample.ll):; ModuleID = 'sample' source_filename = "sample" define i32 @main() { ret i32 0 } - Run the pass:
opt -load-pass-plugin=./libTestPass.so -passes='test-pass' -S -disable-output sample.ll 2>&1Expected output on stderr:
TestPass saw function: mainIf you see this line, the NPM successfully loaded and invoked the pass.
- Introduce a deliberate error: change the return statement to
return PreservedAnalyses::none();while still not modifying the IR. Re-run the command; LLVM should abort with a verification failure, confirming the manager's analysis checks are active. - Compare NPM vs. legacy PM:
opt -O2 -S sample.ll -o opt-npm.bc opt -O2 -S -enable-new-pm=0 sample.ll -o opt-legacy.bc cmp -s opt-npm.bc opt-legacy.bc && echo "IR identical" || echo "IR differs"Identical output indicates the new design preserves semantics while providing extensibility.
Limitations
- The NPM adds a small indirection for analysis caching; in ultra-low-latency JIT scenarios a hand-rolled legacy pass may still be preferable.
- Debugging registration errors can be opaque; enabling
-debug-pass-manageror usingopt -passes=print<module>helps pinpoint mismatches. - Version-specific API changes mean that tutorials targeting older LLVM may need adaptation (e.g., using
PassPluginLibraryInfovs. legacyRegisterPass).
Practical Verification Checklist
- Build LLVM 14 or later, or confirm the pre-built version.
- Compile the pass as a shared library.
- Run
opt -load-pass-plugin=<lib> -passes='test-pass'on a trivial IR file and verify the expected print appears. - Introduce an analysis-preservation mistake and confirm LLVM aborts with a verification error.
- Run
-O2with both NPM and legacy PM and compare the resulting IR binaries.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.