This repository provides a template to create your own LLVM passes using LLVM 21, drawing heavily on llvm-tutor and Writing an LLVM Pass.
It allows you to quickly bootstrap, test, and benchmark new out-of-tree LLVM passes with minimal effort — that is, without the need to build LLVM from sources.
The lib folder contains an example pass called FunctionCounter, which counts the number of functions in the input LLVM IR file and prints the result to the standard output.
You can git clone this repository and start implementing your own LLVM passes right away.
This repository is shipped with:
- An LLVM pass plugin that can be loaded into the
opttool (see Run the pass withoptsection for more details). - An LLVM pass that can be registered as part of an existing LLVM (both
clangandopt) default pipeline (see Run the pass against an existing default LLVM pipeline section for more details). - A standalone tool (
func-count) to run the FunctionCounter pass without relying onopt(see Run the pass as executable section for more details). - A set of unit tests for the FunctionCounter pass using
llvm-litandFileCheck(see Testing section for more details). - A benchmarking setup using
llvm-test-suiteto measure the performance impact of your pass (see Benchmarking with llvm-test-suite section for more details).
This repository includes a utility script called x.sh that streamlines the most common build, test, and usage operations for the LLVM pass.
You can use this script from the root of the repository to quickly execute the main commands without having to remember all the manual instructions.
Note
The script uses the LLVM_DIR environment variable to locate your LLVM installation. If LLVM_DIR is not set, it defaults to ~/dev/llvm-project/llvm-build.
config— configure the project with CMake for a debug build.build— build the project.rebuild— reconfigure and rebuild the project from scratch.run <file.ll>— run the pass on a specific LLVM IR file.test— run the test suite withlit.pipeline [level]— print the optimization pipeline (default: -O0).inject <level> <file.ll>— inject/replace the custom pass in an optimization pipeline.emit-llvm <file.c> [level]— generate LLVM IR from a C file (default: -O0).clean— clean the build directory.help— show the help message and list of commands.
For example, to configure, build, and test the project you can simply run:
./x.sh config
./x.sh build
./x.sh testRun ./x.sh help for the complete and detailed list of available commands.
The LLVM_INSTALLATION.md file includes detailed instructions on how to set up the system to have an LLVM 21 installation, and how to set the LLVM_DIR environment variable accordingly.
For instance:
export LLVM_DIR=/usr/local/opt/llvm@21 # macOS Intel via Homebrew
export LLVM_DIR=/opt/homebrew/opt/llvm@21 # macOS Apple Silicon via Homebrew
export LLVM_DIR=/usr/lib/llvm-21 # Ubuntu x86_64 via apt
export LLVM_DIR=~/llvm-project/llvm-build # LLVM 21 builtTo easily emit human-readable LLVM IR files (.ll) from C/C++ source files, you can use clang:
${LLVM_DIR}/bin/clang -fno-discard-value-names -S -emit-llvm <file>.c -o <file>.llTo build the pass, run the following commands from the root of the repository:
mkdir build
cd build
cmake -DLLVM_INSTALL_DIR=${LLVM_DIR} .. # -G Ninja
make # ninjaNote that, the
buildfolder is hardcoded within.clangdfile to provide better IDE support.clangduses thecompile_commands.jsonfile generated by CMake to provide accurate code completion and navigation features. If you decide to use a different build folder, make sure to update the path in.clangdaccordingly.
By assuming you have built the pass successfully, you can run it using the opt tool provided by your LLVM installation.
To run the pass on an LLVM IR file, use the opt tool as follows:
${LLVM_DIR}/bin/opt \
--load-pass-plugin build/lib/libFunctionCounter.{dylib|so} \
--passes="print<function-counter>" \
-disable-output \
<file>.llNote that,
-disable-outputis used to avoid generating an output file. If you want to generate an output file, replace-disable-outputwith-S -o <output-file>.ll. Assuming you have an LLVM assert build, if your pass leverages the LLVM internal logger, you can either enable logging globally with-debugor selectively for your pass only. To enable logging for your pass only, after the definition of theDEBUG_TYPE=<what-you-want>macro, you can enable logging by passing the-debug-only="<what-you-want>"flag toopt.
Note
This section assume that your pass is registered as a step of an existing LLVM pipeline.
To do so, you need to register your pass within the getFunctionCounterPluginInfo() function by using, for instance, PB.registerPipelineStartEPCallback which registers your pass at the start of a default pipeline.
Of course, there are a plethora of other registration methods you can use depending on where you want to insert your pass within the pipeline.
The FunctionCounter.cpp file registers the FunctionCounter pass by using PB.registerPipelineStartEPCallback as an example.
By assuming you have built the pass successfully, you can leverage an existing LLVM (both clang and opt) default (-O{0,1,2,3,s,z}) pipeline to run the pass as a plugin during the lowerings/optimizations.
${LLVM_DIR}/bin/clang \
-fpass-plugin=build/lib/libFunctionCounter.{dylib|so} \
-fno-discard-value-names \
-O{0,1,2,3,s,z} \ # Pick one optimization level
-S -emit-llvm \
<file>.c -o <file>.llOptionally, you can also specify additional flags:
-Rpass=<pass-name>: to report optimizations performed by the pass.-Rpass-missed=<pass-name>: to report missed optimizations by the pass.-Rpass-analysis=<pass-name>: to report analyses performed by the pass.
Or with opt:
${LLVM_DIR}/bin/opt \
--load-pass-plugin build/lib/libFunctionCounter.{dylib|so} \
--passes="default<O{0,1,2,3,s,z}>" \ # Pick one optimization level
-Rpass=function-counter \
-disable-output \
<file>.llYou can run the analysis pass through a standalone tool called func-count.
func-count is an LLVM based executable defined in
FuncCountMain.cpp.
It is a command line wrapper that allows you to run FunctionCounter without the need for opt:
<build_dir>/bin/func-count <file>.llIt is an example of a relatively basic static analysis tool. Its implementation demonstrates how basic pass management in LLVM works (i.e. it handles that for itself instead of relying on opt).
In order to run the tests, you need to install llvm-lit (i.e., lit).
If you have LLVM built from sources, llvm-lit is located in the <llvm-build-dir>/bin/ folder.
On the other hand, it is not bundled with LLVM 21 packages, but you can install it with pip or brew (on macOS).
{pip | brew} install litTo run the tests, execute the following command from the root of the repository:
lit -v -a ./build/testWarning
The benchmarking setup is specifically for the LLVM FunctionCounter pass provided in this repository. If you are using a different pass, you will need to modify the benchmark.sh script accordingly.
The benchmarking directory contains scripts and documentation (its README.md file) for benchmarking the FunctionCounter LLVM pass using the llvm-test-suite.