Your LLVM Pass Compiled but Never Ran: Diagnosing New Pass Manager Pipeline Failures
Your LLVM pass compiles and registers cleanly but never transforms anything. Diagnose new Pass Manager failures: scheduling, extension points, PreservedAnalyses, and when to escalate upstream.
05 Jul 2025, 09:34 UTC

The recognizable condition
Your custom LLVM pass builds cleanly, registers without a single error, links into opt or clang — and then does nothing. The output IR is byte-for-byte identical to the input, or the transformation you expected silently never fires. There is no crash, no warning, no diagnostic. Under LLVM's new Pass Manager (NPM), this silence is the signature symptom, and it almost always traces back to one of three causes: the pass was never scheduled, it was scheduled at the wrong point, or it ran but poisoned the analysis results for everything after it.
This guide assumes LLVM 17 or later, where the legacy Pass Manager has been removed and NPM is the only option. Check your version first with llvm-config --version — API names and plugin flags differ sharply across releases, so confirm callback signatures against the PassBuilder.h header actually installed on your system before assuming any example below compiles.
Cause and symptom table
| Symptom | Likely cause | First check |
|---|---|---|
Pass absent from -debug-pass-manager output | Registered at the wrong extension point, or plugin never loaded | Confirm plugin flag and registration callback |
| Pass appears in pipeline but IR is unchanged | Pass runs on the wrong IR unit (module vs. function), or its filter never matches | -print-after=<pass-name> |
| Pass runs, but later optimizations produce wrong code | Returned PreservedAnalyses::all() after mutating IR; downstream passes use stale analysis | Audit the return value; run with -verify-each |
| Plugin flag accepted but pass unknown | Mixed up clang's -fpass-plugin with opt's -load-pass-plugin | Re-run with the correct flag for the tool |
Ordered checks
1. Confirm the pass is actually scheduled
Run opt (no special permissions needed; run in the directory containing your plugin and input IR):
opt -load-pass-plugin=./MyPass.so -passes=my-pass \
-debug-pass-manager input.ll -S -o /dev/null./MyPass.so is the path to your built plugin; my-pass is the name you registered. The -debug-pass-manager output lists every pass as it is scheduled. If your pass name does not appear, the problem is registration or loading — stop here and fix that before investigating transformation logic. If you are testing through clang instead, the flag is -fpass-plugin=./MyPass.so, not -load-pass-plugin; on some versions the wrong flag fails silently, which produces exactly the "compiles but never runs" condition.
2. Dump IR around the pass
If the pass is scheduled, verify what it actually does:
opt -load-pass-plugin=./MyPass.so -passes=my-pass \
-print-before=my-pass -print-after=my-pass input.ll -S -o /dev/nullCompare the two dumps against a hand-checked expectation. Identical before/after IR means the pass ran but its matching logic never fired — a common case is a FunctionPass whose entry condition (name check, attribute check) matches nothing in your test file.
3. Audit PreservedAnalyses
If the pass mutates IR, its run() method must not return PreservedAnalyses::all(). Doing so tells the analysis manager that cached results (dominator trees, alias analysis, loop info) are still valid when they are not. Later passes then optimize against a stale view of the program, producing subtle miscompiles rather than crashes — the worst kind of bug to catch in review. When in doubt, return PreservedAnalyses::none(); it costs recomputation but is always safe. Refine to a precise preserved set only once the pass is proven correct.
4. Run the verifier between every pass
opt -load-pass-plugin=./MyPass.so -passes='my-pass,default<O2>' \
-verify-each input.ll -S -o out.ll-verify-each runs the IR verifier after each pass in the pipeline. Any structural corruption your pass introduces — a dangling use, a broken PHI node — is pinned to the exact pass that caused it, instead of surfacing as a mysterious crash three passes later.
Fixes tied to findings
Pass never scheduled: move registration to the correct PassBuilder callback. A frequent mistake is registering a function-level transformation via registerPipelineStartEPCallback (which expects module-level pipeline elements) when it belongs in a function pipeline extension point such as registerOptimizerEarlyEPCallback or a parse-callback registered with registerPipelineParsingCallback. The failure mode is silence, not an error, because the callback simply never fires for your pass kind. Also prefer the plugin mechanism (-load-pass-plugin / -fpass-plugin) over statically patching and rebuilding LLVM — plugins isolate your code from the host build and make version mismatches obvious at load time.
Pass scheduled but inert: fix the matching logic or the IR-unit granularity. If you need function-level behavior inside a module pipeline, wrap it with createModuleToFunctionPassAdaptor rather than re-registering elsewhere.
Stale analysis: return an accurate PreservedAnalyses set, re-run with -verify-each, and re-test the full optimization pipeline, not just your pass in isolation — invalidation bugs only manifest when something downstream consumes the stale result.
When to escalate upstream
Escalate only after all of the following hold: the pass appears in -debug-pass-manager output, -verify-each passes across the whole pipeline, and the output is still wrong. Then reduce the reproducer to a minimal .ll file that triggers the problem with opt alone — no clang, no build system, no plugin if the issue reproduces with stock passes. If the same reproducer misbehaves on two recent LLVM major versions, file a GitHub issue on llvm/llvm-project with the IR file, the exact opt command, and the expected versus actual output. If it reproduces on only one version, check the release notes and recent commits first; you may be looking at an already-fixed bug or an intentional behavior change.
Limitations
Callback names, extension points, and plugin flags have changed across LLVM releases; every command above should be validated against your installed headers and opt --help output. Nothing here was executed against a specific build — treat the flag spellings as a starting point and confirm them on your toolchain before scripting around them.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.