LLVM passes
Contract
| Field | Bound contract |
|---|---|
| Trigger | A user adds a custom transform or analysis to an LLVM-based compiler, needs a plugin that opt -load-pass-plugin accepts, uses dominators, loops, or alias analysis inside a pass, or debugs a pass that corrupts IR or leaves analyses stale. |
| Authority | Reversible local: writes only the pass source, its CMake file, and its lit tests inside the project, plus the build directory; rollback is version control and deleting the build directory. No remote mutation. |
| Side effect | A plugin shared object is built and run through opt on test IR; the verifier and the lit test decide the result. |
| Done | The plugin loads into the installed opt, the pass name resolves in -passes=, -verify-each reports no broken IR on the test inputs, and the lit test passes. |
Inputs
- The transform or analysis to implement (required): what IR pattern it matches and what it produces or reports.
- Pass scope (required if not obvious): per function or per module.
- Test IR (required): a
.llinput that contains the pattern and one that does not. - Installed LLVM (gathered by the skill):
llvm-config --versionand--cmakedir. Grounded current stable is LLVM 23.1.0; header paths, macros, and flags below are confirmed against it. The plugin must build against the same LLVM thatoptwas built from.
Procedure
Fix the scope. Under the new pass manager, a
ModulePassManagerruns module passes and nests aFunctionPassManagerper function; anAnalysisManagercaches analyses and invalidates them when a pass reports that it did not preserve them. A pass that reads only one function at a time is a function pass; a pass that adds globals or functions is a module pass. Done when: the scope is stated with the reason.Write the pass as a
PassInfoMixinwith arunmethod, and export the plugin entry point. The plugin header lives atllvm/Plugins/PassPlugin.hin LLVM 23.1.0. Erase instructions throughmake_early_inc_range; erasing inside a plain range loop invalidates the iterator.#include "llvm/ADT/STLExtras.h" #include "llvm/IR/PassManager.h" #include "llvm/Passes/PassBuilder.h" #include "llvm/Plugins/PassPlugin.h" using namespace llvm; namespace { struct RemoveDeadFuncCalls : PassInfoMixin<RemoveDeadFuncCalls> { PreservedAnalyses run(Function &F, FunctionAnalysisManager &) { bool Changed = false; for (BasicBlock &BB : F) for (Instruction &I : make_early_inc_range(BB)) if (auto *Call = dyn_cast<CallInst>(&I)) if (Function *Callee = Call->getCalledFunction(); Callee && Callee->getName() == "dead_func") { Call->eraseFromParent(); Changed = true; } return Changed ? PreservedAnalyses::none() : PreservedAnalyses::all(); } }; } // namespace extern "C" LLVM_ATTRIBUTE_WEAK PassPluginLibraryInfo llvmGetPassPluginInfo() { return {LLVM_PLUGIN_API_VERSION, "RemoveDeadFuncCalls", "v0.1", [](PassBuilder &PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager &FPM, ArrayRef<PassBuilder::PipelineElement>) { if (Name != "remove-dead-func-calls") return false; FPM.addPass(RemoveDeadFuncCalls()); return true; }); }}; }A module pass has the signature
PreservedAnalyses run(Module &M, ModuleAnalysisManager &)and registers through theModulePassManageroverload of the same callback. Skip declarations withF.isDeclaration()when iterating a module. Done when: the source compiles with the pipeline name chosen.Build it as a
MODULElibrary against the installed LLVM:cmake_minimum_required(VERSION 3.20) project(RemoveDeadFuncCalls C CXX) find_package(LLVM REQUIRED CONFIG) add_library(RemoveDeadFuncCalls MODULE RemoveDeadFuncCalls.cpp) target_include_directories(RemoveDeadFuncCalls SYSTEM PRIVATE ${LLVM_INCLUDE_DIRS}) target_compile_definitions(RemoveDeadFuncCalls PRIVATE ${LLVM_DEFINITIONS}) set_target_properties(RemoveDeadFuncCalls PROPERTIES CXX_STANDARD 17)cmake -B build -DLLVM_DIR=$(llvm-config --cmakedir) cmake --build buildCMake prefixes the module with
lib. The plugin needs no explicit LLVM link line:optprovides the symbols at load time, and the build above links and loads on LLVM 23.1.0. Done when:build/libRemoveDeadFuncCalls.soexists.Run it through
opt, alone and inside a pipeline, with the verifier on:opt -load-pass-plugin ./build/libRemoveDeadFuncCalls.so -passes=remove-dead-func-calls -verify-each -S input.ll -o output.ll opt -load-pass-plugin ./build/libRemoveDeadFuncCalls.so -passes='function(instcombine,remove-dead-func-calls,dce)' -S input.ll -o out.ll-Swrites textual IR; without it the output is bitcode. Inside a pipeline string a function pass must sit insidefunction(...); naming it at module level fails with "unknown module pass". Done when: the pattern is gone fromoutput.lland-verify-eachprints nothing.Use analyses through the analysis manager; the dependency is tracked by the request, so no
getAnalysisUsagedeclaration exists under the new pass manager:#include "llvm/Analysis/AliasAnalysis.h" #include "llvm/Analysis/LoopInfo.h" #include "llvm/IR/Dominators.h" PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM) { auto &DT = AM.getResult<DominatorTreeAnalysis>(F); auto &LI = AM.getResult<LoopAnalysis>(F); auto &AA = AM.getResult<AAManager>(F); for (Loop *L : LI) { BasicBlock *Header = L->getHeader(); /* hoist here */ } DomTreeNode *Root = DT.getNode(&F.getEntryBlock()); return PreservedAnalyses::all(); }Return
PreservedAnalyses::all()only when the IR is unchanged. After a structural change, either returnnone()or name the analyses that survive (PA.preserve<DominatorTreeAnalysis>()). Done when: each analysis the pass reads is requested throughAM, and the preserved set matches what the pass changed.Modify IR through the builder and the standard utilities, and keep SSA intact:
IRBuilder<> Builder(&I); Value *NewVal = Builder.CreateAdd(I.getOperand(0), ConstantInt::get(I.getType(), 1)); I.replaceAllUsesWith(NewVal); I.eraseFromParent(); ValueToValueMapTy VMap; BasicBlock *Clone = CloneBasicBlock(OrigBB, VMap, ".clone", &F); // llvm/Transforms/Utils/Cloning.h FunctionCallee Fn = M.getOrInsertFunction("my_fn", FunctionType::get(Builder.getVoidTy(), false));Replace every use before erasing a value; an erased value with remaining uses is the verifier's "use of undefined value" failure. Done when:
-verify-eachpasses on every test input after the change.Write a lit test with FileCheck.
litis a Python package (pip install lit) andFileCheckships with LLVM; alit.cfg.pynext to the tests defines the%shlibdirsubstitution to the build directory.; RUN: opt -load-pass-plugin %shlibdir/libRemoveDeadFuncCalls.so -passes=remove-dead-func-calls -S %s | FileCheck %s declare void @dead_func() define void @test() { call void @dead_func() ret void } ; CHECK-LABEL: define void @test() ; CHECK-NOT: dead_func ; CHECK: ret voidlit test/ -vDone when: the lit run reports the test as passed.
When the pass misbehaves, use the pass manager's own instrumentation before adding prints:
opt -load-pass-plugin ./build/libRemoveDeadFuncCalls.so -passes=remove-dead-func-calls -print-after-all input.ll -o /dev/null opt -passes=remove-dead-func-calls -time-passes input.ll -o /dev/null opt -passes=remove-dead-func-calls -debug-pass-manager input.ll -o /dev/null-print-after-allshows the IR after every pass;-debug-pass-managershows which passes and analyses ran and which were invalidated. Done when: the first pass whose output is wrong is identified.
Failure and recovery
| Failure class | Behavior |
|---|---|
Pass not found |
The plugin did not load or the pipeline name differs from the string in the parsing callback. Confirm the .so path and the name; opt --print-passes does not list plugin passes. |
| Verifier failure | SSA broken by the transform. Run with -verify-each, read the first message, and fix the use-def order in the transform. |
| Analysis stale | The pass changed IR and returned all(). Return none() or an explicit preserved set. |
| Plugin fails to load | Built against a different LLVM than opt. Rebuild with -DLLVM_DIR=$(llvm-config --cmakedir) for the opt in use. |
| Empty or binary output | -S missing. Add it for textual IR. |
| lit test fails on CHECK | The pass output differs from the expectation. Run the RUN line by hand with -S and compare before editing CHECK lines. |
No partial result is claimed complete. If a step cannot finish, the report states which steps passed and which are blocked.
Output
A pass delivery containing:
- Files written: the pass source, CMake file, and lit tests.
- Build and load evidence: the
optcommand that loaded the plugin and the pipeline string used. - Verification: the
-verify-eachresult on each test input and the lit result. - Preserved analyses: the set the pass declares and why.