Compare LLVM IR Structure Safely with llvm-diff
llvm-diff-18 answers one question in a way you can script: did this transformation actually change the structure of the module. No output and exit status 0 means it found no structural difference it could diagnose; non-zero means it found one, or could not complete the comparison. This guide uses the llvm-18 package on Ubuntu, which provides llvm-diff-18 version 18.1.3.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about 10 minutes if you already have two modules. You need a shell, the llvm-18 package, and two compatible LLVM assembly files or bitcode files. No root access is required. The command is a debugging aid for LLVM pass and frontend work, not a general-purpose semantic equivalence checker.
1. Check the installed tool
Run the version check before you trust any example, including this one. The output format is not a stable interface, so record the package version with any diagnostic you pass to somebody else:
$ llvm-diff-18 --version
Ubuntu LLVM version 18.1.3
Optimized build.
The corresponding local package is llvm-18 1:18.1.3-1ubuntu1 on the machine used for this guide. A different LLVM release can carry different diagnostics and different blind spots.
2. Compare two LLVM assembly modules
Put the modules in files whose names end in .ll. That suffix matters: llvm-diff reads a file ending in .ll as LLVM assembly, and anything else as bitcode, so renaming a bitcode file to look like assembly can make the command fail outright.
$ llvm-diff-18 build/before.ll build/after.ll
$ status=$?
$ printf 'exit status: %s\n' "$status"
exit status: 0
When the modules match on the structures llvm-diff checks, the normal result is no output and status 0. Capture the status immediately after the command: running printf first would quietly replace $? with printf's own status.
3. See what an actual difference looks like
Change a small, intentional part of a test module and run the comparison again:
$ llvm-diff-18 build/before.ll build/after.ll
... diagnostic text describing a structural difference ...
$ printf 'exit status: %s\n' "$?"
exit status: 1
The exact diagnostic is deliberately not shown here as a stable transcript, because the LLVM documentation itself says llvm-diff's output format is not stable. Treat the non-zero status as your automation signal, and read the text purely as a human debugging hint.
Checkpoint
You have now seen both ends of the contract, a clean status 0 for a real match and a non-zero status once you deliberately broke it. That pairing is the whole safety model for scripting against this tool.
4. Limit the comparison to named globals
With no names after the two filenames, llvm-diff compares all global values and reports any that exist on only one side. Add one or more global names when you are chasing a particular function or global, using the spelling that appears in the LLVM IR, including a leading @ where the IR uses one:
$ llvm-diff-18 build/before.ll build/after.ll @calculate
$ llvm-diff-18 build/before.ll build/after.ll @calculate @helper
This narrows the work and the diagnostic, but it is not proof that the selected functions behave identically. The tool follows basic blocks from entry blocks, and when terminators do not appear to match, downstream blocks are skipped, so an early control-flow change can hide later differences entirely.
5. Use the result in a shell check
For a CI or regression check, branch on the exit status and keep the tool's diagnostic on standard output or error as your surrounding script needs. This example stops at the first detected difference:
$ if llvm-diff-18 build/before.ll build/after.ll; then
> echo 'no diagnosed structural differences'
> else
> rc=$?
> echo "llvm-diff found a difference or failed (status $rc)" >&2
> exit "$rc"
> fi
no diagnosed structural differences
Warning
Do not write a check that treats every non-zero status as proof of a semantic regression without also considering malformed input, a wrong suffix, or an unsupported module feature. Rerun the command directly and read the diagnostic before you believe it.
What llvm-diff does not promise
The comparison is structural and focuses mainly on function definitions. It ignores some genuinely insignificant changes, such as global ordering and local value names, but it also fails to diagnose every meaningful change: the installed manpage specifically calls out linkage and function attributes as things it can miss. Memory-behaviour changes such as coalesced loads can instead produce enormous, unhelpful block differences.
That boundary is the main safety rule here. Use llvm-diff to investigate how an LLVM transformation changed a module, then reach for a proper verifier, test suite, or domain-specific equivalence check for the property you actually care about. Do not treat empty output as permission to skip that step.
Common traps
- Both files are named
.llbut hold bitcode. Rename them to a non-.llsuffix or regenerate assembly with the matching LLVM tool: the suffix is what controls the input reader, nothing else. - The command reports a difference everywhere. Check whether control flow changed early, whether memory operations were rearranged, and whether the two modules came from compatible LLVM pipelines.
- You expected attribute or linkage changes to show up. They are a known blind spot here. There is nothing destructive to undo; switch to a checker that actually covers those properties.
- You want coloured output. The installed help lists an explicit colour option, with automatic detection as the default. Fine for an interactive terminal, but do not depend on colouring inside logs.
$ llvm-diff-18 --color build/before.ll build/after.ll
Done means
- Version confirmed. You checked which llvm-diff-18 version is installed.
- Inputs are correct. You supplied two correctly named LLVM modules and understand the
.llsuffix rule. - Status checked, not just text. You checked the exit status rather than relying on whether anything printed.
- Scope is deliberate. You narrowed the comparison with global names only when that matched the actual debugging question.
- Limits respected. You treated an empty result as "no diagnosed difference", never as a complete semantic proof.