Blogging site
As someone building LLVM passes, I wanted to get hands-on with ValueTracking —
the analysis utilities LLVM uses to deduce properties like non-zeroness,
sign bits, or floating-point classifications.
What looked simple quickly turned into an odyssey through API churn and
obscure errors.
When I first set out to explore LLVM’s ValueTracking utilities, I thought i’d spend an afternoon printing known bits, sign bits, and floating-point classifications. Instead, I got a week-long crash course in LLVM’s ever-changing APIs, confusing error messages and how the compiler really reasons about values.
This post is a record of that journey — the stumbles, fixes and lessons learned.
LLVM’s ValueTracking in (in llvm/Analysis/ValueTracking.h
) is a set of fast
reasoning helpers that answer questions like:
These queries drive optimizations in InstCombine
, GVN
, and others.
I wanted to see them in action by writing a toy pass: VTPrinter
.
The pass walks each instruction in a function and prints what ValueTracking
can prove.
The idea was simple:
KnownBits KB = computeKnownBits(&I, DL, 0, &AC, &I, &DT);
unsigned NSB = computeNumSignBits(&I, DL, 0, &AC, &I, &DT);
bool NonZero = isKnownNonZero(&I, DL, 0, &AC, &I, &DT);
bool IsPow2 = isKnownToBeAPowerOfTwo(&I, DL, false, 0, &AC, &I, &DT);
KnownFPClass FC = computeKnownFPClass(&I, DL, 0, &AC, &I, &DT);
Add a bit of errs()
logging and voila ! — we would have a basic teaching tool.
I quickly hit the first wall: APIs had changed.
APInt::toString
no longer returns a std::string
. In LLVM 20, it writes into a SmallString
:
SmallString<64> Z, O;
KB.Zero.toString(Z, 16, false);
KB.One.toString(O, 16, false);
errs() << "KnownZero=0x" << Z << " KnownOne=0x" << O << "\n";
getDemandedBits
which returns KnownBits
and no longer a Value*
.DataLayout
, AssumptionCache
,
and DominatorTree
not just a SimplifyQuery
.isGuaranteedNotToBeUndefOrPoison(Value*, SimplifyQuery)`
KnownFPClass::cannotBe(…) doesn’t exist anymore. Use the family of isKnownNever*helpers:
FC.isKnownNeverNaN();
FC.isKnownNeverNegativeZero();
FC.isKnownNeverInfinity();
FC.isKnownNeverSubnormal();
FC.isKnownNeverZero();
The compilation, when succeeded, will generate a library which can be passed to the opt (LLVM optimizer). The following example is a test that’s expressed in LLVM IR which we run against (see source):
declare void @llvm.assume(i1)
define i32 @demo(i32 %x, i32 %y) {
entry:
; Assume x >= 1 (=> nonzero, clears sign info)
%cmp = icmp sge i32 %x, 1
call void @llvm.assume(i1 %cmp)
; pow2 = x & -x -> known power-of-two (lowbit)
%negx = sub i32 0, %x
%pow2 = and i32 %x, %negx
; high = shl nuw i32 1, 8 ; NUW helps known bits
%high = shl nuw i32 1, 8
; mix = or i32 %pow2, %high
%mix = or i32 %pow2, %high
; only low 8 bits used later (lets demanded-bits fold)
%trunc = trunc i32 %mix to i8
%zext = zext i8 %trunc to i32
; FP: start from positive finite number
%f = fadd nnan nsz float 1.0, 2.0 ; no NaN, not negative zero
%g = fmul nnan nsz float %f, 0x3F800000 ; *1.0
ret i32 %zext
}
define i32 @ptr_demo(i32* nonnull align 16 %p) {
entry:
; load from a nonnull, 16-aligned pointer
%v = load i32, i32* %p, align 16
ret i32 %v
}
Runtime Surprise: IR Parsing Error
Even after fixing compilation, running opt
failed with:
opt: ./test.ll:29:32: error: floating point constant invalid for type
%g = fmul nnan nsz float %f, 0x3F800000 ; *1.0
Turns out: 0x3F800000 is not a valid float literal in the LLVM IR. It’s the bit pattern for 1.0f, what works was to express it as a literal 1.000000e+00
ValueTracking.h
since
it’s the only single source of truth.SimplifyQuery
might work in one version but not the next.
Be explicit with DL
, AC
, DT
and CtxI
. which represents the
DataLayout
, AssumptionCache
, DominatorTree
and the (earlier)
query’s context respectively.#if LLVM_VERSION_MAJOR
is your
friend (see below for how its typically done)
# if LLVM_VERSION_MAJOR >= 20
bool NotPoisonOrUndef =
isGuaranteedNotToBeUndefOrPoison(&I, &AC, &I, &DT);
# else
bool NotPoisonOrUndef = isGuaranteedNotToBeUndefOrPoison(&I, Q);
# endif
errs() << " NotUndefOrPoison: " << (NotPoisonOrUndef ? "yes" : "no")
<< "\n";
}
After all the bumps, I had a working LLVM plugin pass (following the new
PassManager
conventions):
Prints KnownBits (zero/one masks)
Show non-zero facts, number of sign bits, and power-of-two detection.
Reports floating point restrictions
Handles demanded bits
Avoids poison / undef
Checkout the source for the driver program. Here’s a short excerpt from a sample run
Running: opt -load-pass-plugin=build/VTPrinter.dylib -passes=vt-printer -o - ./test.ll
...
=== VTPrinter (LLVM 20.1.8) on function: demo ===
Instruction analyzed: %cmp = icmp sge i32 %x, 1
KnownZero: 0x0
KnownOne : 0x0
KnownNonZero: no
NumSignBits: 1
IsPowerOfTwo: no
NotUndefOrPoison: yes
Instruction analyzed: call void @llvm.assume(i1 %cmp)
NotUndefOrPoison: no
Instruction analyzed: %negx = sub i32 0, %x
KnownZero: 0x0
KnownOne : 0x0
KnownNonZero: yes
NumSignBits: 1
IsPowerOfTwo: no
Ones in demanded bits: 0
Zeros in demanded bits: 0
NotUndefOrPoison: no
... other output omitted