Taming ValueTracking in LLVM20: a beginner’s notes

Blogging site

Taming ValueTracking in LLVM20: a beginner’s notes

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.

Why ValueTracking?

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 Plan: A Printing Pass

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.

The First Compilation Errors

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";
isGuaranteedNotToBeUndefOrPoison(Value*, SimplifyQuery)`
KnownFPClass::cannotBe() doesnt 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

Lessons Learned


# 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";
    }

Final Result

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