[llvm] [ValueTracking] Set KnownBits from MSB shift idiom (PR #200203)

Marina Taylor via llvm-commits llvm-commits at lists.llvm.org
Mon Jun 8 06:13:46 PDT 2026


https://github.com/citymarina updated https://github.com/llvm/llvm-project/pull/200203

>From ef78e8d4f52ef9ba6a68a9cb4401bd28dd7a3b1b Mon Sep 17 00:00:00 2001
From: Marina Taylor <marina_taylor at apple.com>
Date: Tue, 26 May 2026 20:18:58 +0100
Subject: [PATCH 1/3] [ValueTracking] Pre-commit test for shl-ctlz-msb idiom
 (NFC)

Assisted-by: claude
---
 .../ValueTracking/knownbits-shl-ctlz-msb.ll   | 167 ++++++++++++++++++
 1 file changed, 167 insertions(+)
 create mode 100644 llvm/test/Analysis/ValueTracking/knownbits-shl-ctlz-msb.ll

diff --git a/llvm/test/Analysis/ValueTracking/knownbits-shl-ctlz-msb.ll b/llvm/test/Analysis/ValueTracking/knownbits-shl-ctlz-msb.ll
new file mode 100644
index 0000000000000..f6bd3c7c3cc5e
--- /dev/null
+++ b/llvm/test/Analysis/ValueTracking/knownbits-shl-ctlz-msb.ll
@@ -0,0 +1,167 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
+; RUN: opt -passes=instcombine -S < %s | FileCheck %s
+
+; Pattern: shl X, sub(Y, xor(ctlz(X), BitWidth-1))
+; When BitWidth is a power of 2 and Y < BitWidth-1, the result has its MSB
+; at bit Y: bit Y is set and bits above Y are clear.
+
+declare i16 @llvm.ctlz.i16(i16, i1 immarg)
+declare i31 @llvm.ctlz.i31(i31, i1 immarg)
+declare i32 @llvm.ctlz.i32(i32, i1 immarg)
+declare i64 @llvm.ctlz.i64(i64, i1 immarg)
+
+define i32 @msb_at_14_bit_set(i32 %x) {
+; CHECK-LABEL: @msb_at_14_bit_set(
+; CHECK-NEXT:    [[C:%.*]] = call range(i32 0, 33) i32 @llvm.ctlz.i32(i32 [[X:%.*]], i1 true)
+; CHECK-NEXT:    [[XORED:%.*]] = xor i32 [[C]], 31
+; CHECK-NEXT:    [[SHAMT:%.*]] = sub nsw i32 14, [[XORED]]
+; CHECK-NEXT:    [[S:%.*]] = shl i32 [[X]], [[SHAMT]]
+; CHECK-NEXT:    [[R:%.*]] = and i32 [[S]], 16384
+; CHECK-NEXT:    ret i32 [[R]]
+;
+  %c = call i32 @llvm.ctlz.i32(i32 %x, i1 true)
+  %xored = xor i32 %c, 31
+  %shamt = sub i32 14, %xored
+  %s = shl i32 %x, %shamt
+  %r = and i32 %s, 16384
+  ret i32 %r
+}
+
+define i32 @msb_at_14_high_bits_clear(i32 %x) {
+; CHECK-LABEL: @msb_at_14_high_bits_clear(
+; CHECK-NEXT:    [[C:%.*]] = call range(i32 0, 33) i32 @llvm.ctlz.i32(i32 [[X:%.*]], i1 true)
+; CHECK-NEXT:    [[XORED:%.*]] = xor i32 [[C]], 31
+; CHECK-NEXT:    [[SHAMT:%.*]] = sub nsw i32 14, [[XORED]]
+; CHECK-NEXT:    [[S:%.*]] = shl i32 [[X]], [[SHAMT]]
+; CHECK-NEXT:    [[R:%.*]] = and i32 [[S]], -32768
+; CHECK-NEXT:    ret i32 [[R]]
+;
+  %c = call i32 @llvm.ctlz.i32(i32 %x, i1 true)
+  %xored = xor i32 %c, 31
+  %shamt = sub i32 14, %xored
+  %s = shl i32 %x, %shamt
+  %r = and i32 %s, 4294934528
+  ret i32 %r
+}
+
+define i32 @msb_at_14_xor_commuted(i32 %x) {
+; CHECK-LABEL: @msb_at_14_xor_commuted(
+; CHECK-NEXT:    [[C:%.*]] = call range(i32 0, 33) i32 @llvm.ctlz.i32(i32 [[X:%.*]], i1 true)
+; CHECK-NEXT:    [[XORED:%.*]] = xor i32 [[C]], 31
+; CHECK-NEXT:    [[SHAMT:%.*]] = sub nsw i32 14, [[XORED]]
+; CHECK-NEXT:    [[S:%.*]] = shl i32 [[X]], [[SHAMT]]
+; CHECK-NEXT:    [[R:%.*]] = and i32 [[S]], -32768
+; CHECK-NEXT:    ret i32 [[R]]
+;
+  %c = call i32 @llvm.ctlz.i32(i32 %x, i1 true)
+  %xored = xor i32 31, %c
+  %shamt = sub i32 14, %xored
+  %s = shl i32 %x, %shamt
+  %r = and i32 %s, 4294934528
+  ret i32 %r
+}
+
+define i32 @msb_at_14_zero_not_undef(i32 %x) {
+; CHECK-LABEL: @msb_at_14_zero_not_undef(
+; CHECK-NEXT:    [[C:%.*]] = call range(i32 0, 33) i32 @llvm.ctlz.i32(i32 [[X:%.*]], i1 false)
+; CHECK-NEXT:    [[XORED:%.*]] = xor i32 [[C]], 31
+; CHECK-NEXT:    [[SHAMT:%.*]] = sub nsw i32 14, [[XORED]]
+; CHECK-NEXT:    [[S:%.*]] = shl i32 [[X]], [[SHAMT]]
+; CHECK-NEXT:    [[R:%.*]] = and i32 [[S]], 16384
+; CHECK-NEXT:    ret i32 [[R]]
+;
+  %c = call i32 @llvm.ctlz.i32(i32 %x, i1 false)
+  %xored = xor i32 %c, 31
+  %shamt = sub i32 14, %xored
+  %s = shl i32 %x, %shamt
+  %r = and i32 %s, 16384
+  ret i32 %r
+}
+
+define <2 x i32> @msb_at_14_vec(<2 x i32> %x) {
+; CHECK-LABEL: @msb_at_14_vec(
+; CHECK-NEXT:    [[C:%.*]] = call range(i32 0, 33) <2 x i32> @llvm.ctlz.v2i32(<2 x i32> [[X:%.*]], i1 true)
+; CHECK-NEXT:    [[XORED:%.*]] = xor <2 x i32> [[C]], splat (i32 31)
+; CHECK-NEXT:    [[SHAMT:%.*]] = sub nsw <2 x i32> splat (i32 14), [[XORED]]
+; CHECK-NEXT:    [[S:%.*]] = shl <2 x i32> [[X]], [[SHAMT]]
+; CHECK-NEXT:    [[R:%.*]] = and <2 x i32> [[S]], splat (i32 16384)
+; CHECK-NEXT:    ret <2 x i32> [[R]]
+;
+  %c = call <2 x i32> @llvm.ctlz.v2i32(<2 x i32> %x, i1 true)
+  %xored = xor <2 x i32> %c, <i32 31, i32 31>
+  %shamt = sub <2 x i32> <i32 14, i32 14>, %xored
+  %s = shl <2 x i32> %x, %shamt
+  %r = and <2 x i32> %s, <i32 16384, i32 16384>
+  ret <2 x i32> %r
+}
+
+; Negative: Y is not less than BitWidth - 1
+define i32 @neg_y_too_large(i32 %x) {
+; CHECK-LABEL: @neg_y_too_large(
+; CHECK-NEXT:    [[C:%.*]] = call range(i32 0, 33) i32 @llvm.ctlz.i32(i32 [[X:%.*]], i1 true)
+; CHECK-NEXT:    [[S:%.*]] = shl i32 [[X]], [[C]]
+; CHECK-NEXT:    [[R:%.*]] = and i32 [[S]], -2147483648
+; CHECK-NEXT:    ret i32 [[R]]
+;
+  %c = call i32 @llvm.ctlz.i32(i32 %x, i1 true)
+  %xored = xor i32 %c, 31
+  %shamt = sub i32 31, %xored
+  %s = shl i32 %x, %shamt
+  %r = and i32 %s, 2147483648
+  ret i32 %r
+}
+
+; Negative: BitWidth not a power of two
+define i31 @neg_bad_width(i31 %x) {
+; CHECK-LABEL: @neg_bad_width(
+; CHECK-NEXT:    [[C:%.*]] = call range(i31 0, 32) i31 @llvm.ctlz.i31(i31 [[X:%.*]], i1 true)
+; CHECK-NEXT:    [[XORED:%.*]] = xor i31 [[C]], 30
+; CHECK-NEXT:    [[SHAMT:%.*]] = sub nsw i31 14, [[XORED]]
+; CHECK-NEXT:    [[S:%.*]] = shl i31 [[X]], [[SHAMT]]
+; CHECK-NEXT:    [[R:%.*]] = and i31 [[S]], 16384
+; CHECK-NEXT:    ret i31 [[R]]
+;
+  %c = call i31 @llvm.ctlz.i31(i31 %x, i1 true)
+  %xored = xor i31 %c, 30
+  %shamt = sub i31 14, %xored
+  %s = shl i31 %x, %shamt
+  %r = and i31 %s, 16384
+  ret i31 %r
+}
+
+; Negative: xor with a constant other than BitWidth-1 breaks the equivalence
+; ctlz(X) ^ K == (BitWidth-1) - ctlz(X), so the MSB position claim doesn't hold.
+define i32 @neg_wrong_xor_const(i32 %x) {
+; CHECK-LABEL: @neg_wrong_xor_const(
+; CHECK-NEXT:    [[C:%.*]] = call range(i32 0, 33) i32 @llvm.ctlz.i32(i32 [[X:%.*]], i1 true)
+; CHECK-NEXT:    [[XORED:%.*]] = xor i32 [[C]], 30
+; CHECK-NEXT:    [[SHAMT:%.*]] = sub nsw i32 14, [[XORED]]
+; CHECK-NEXT:    [[S:%.*]] = shl i32 [[X]], [[SHAMT]]
+; CHECK-NEXT:    [[R:%.*]] = and i32 [[S]], 16384
+; CHECK-NEXT:    ret i32 [[R]]
+;
+  %c = call i32 @llvm.ctlz.i32(i32 %x, i1 true)
+  %xored = xor i32 %c, 30
+  %shamt = sub i32 14, %xored
+  %s = shl i32 %x, %shamt
+  %r = and i32 %s, 16384
+  ret i32 %r
+}
+
+; Negative: the value being shifted is not the value fed to ctlz.
+define i32 @neg_different_x(i32 %x, i32 %y) {
+; CHECK-LABEL: @neg_different_x(
+; CHECK-NEXT:    [[C:%.*]] = call range(i32 0, 33) i32 @llvm.ctlz.i32(i32 [[Y:%.*]], i1 true)
+; CHECK-NEXT:    [[XORED:%.*]] = xor i32 [[C]], 31
+; CHECK-NEXT:    [[SHAMT:%.*]] = sub nsw i32 14, [[XORED]]
+; CHECK-NEXT:    [[S:%.*]] = shl i32 [[X:%.*]], [[SHAMT]]
+; CHECK-NEXT:    [[R:%.*]] = and i32 [[S]], 16384
+; CHECK-NEXT:    ret i32 [[R]]
+;
+  %c = call i32 @llvm.ctlz.i32(i32 %y, i1 true)
+  %xored = xor i32 %c, 31
+  %shamt = sub i32 14, %xored
+  %s = shl i32 %x, %shamt
+  %r = and i32 %s, 16384
+  ret i32 %r
+}

>From bdc200dd2ce76d28185392e83583affc00f42f5b Mon Sep 17 00:00:00 2001
From: Marina Taylor <marina_taylor at apple.com>
Date: Wed, 27 May 2026 19:58:58 +0100
Subject: [PATCH 2/3] [ValueTracking] Set KnownBits from MSB shift idiom

`shl X, sub(Y, xor(ctlz(X), BitWidth-1))` shifts X so that its MSB lands
at bit Y, when BitWidth is a power of 2.

https://alive2.llvm.org/ce/z/NTVYH7

Assisted-by: claude
---
 llvm/lib/Analysis/ValueTracking.cpp           | 14 ++++++++
 .../ValueTracking/knownbits-shl-ctlz-msb.ll   | 35 +++----------------
 2 files changed, 19 insertions(+), 30 deletions(-)

diff --git a/llvm/lib/Analysis/ValueTracking.cpp b/llvm/lib/Analysis/ValueTracking.cpp
index 3be0db5ad9c7e..a5643a14bf435 100644
--- a/llvm/lib/Analysis/ValueTracking.cpp
+++ b/llvm/lib/Analysis/ValueTracking.cpp
@@ -1633,6 +1633,20 @@ static void computeKnownBitsFromOperator(const Operator *I,
     const APInt *C;
     if (match(I->getOperand(0), m_APInt(C)))
       Known.Zero.setLowBits(C->countr_zero());
+
+    // shl X, sub(Y, xor(ctlz(X), BitWidth-1)) shifts X so that its MSB lands
+    // at bit Y, when BitWidth is a power of 2.
+    const APInt *YC;
+    Value *X = I->getOperand(0);
+    if (isPowerOf2_32(BitWidth) &&
+        match(I->getOperand(1),
+              m_Sub(m_APInt(YC), m_c_Xor(m_Ctlz(m_Specific(X), m_Value()),
+                                         m_SpecificInt(BitWidth - 1)))) &&
+        YC->ult(BitWidth - 1)) {
+      unsigned Y = YC->getZExtValue();
+      Known.One.setBit(Y);
+      Known.Zero.setBitsFrom(Y + 1);
+    }
     break;
   }
   case Instruction::LShr: {
diff --git a/llvm/test/Analysis/ValueTracking/knownbits-shl-ctlz-msb.ll b/llvm/test/Analysis/ValueTracking/knownbits-shl-ctlz-msb.ll
index f6bd3c7c3cc5e..9a101d945c1ed 100644
--- a/llvm/test/Analysis/ValueTracking/knownbits-shl-ctlz-msb.ll
+++ b/llvm/test/Analysis/ValueTracking/knownbits-shl-ctlz-msb.ll
@@ -12,12 +12,7 @@ declare i64 @llvm.ctlz.i64(i64, i1 immarg)
 
 define i32 @msb_at_14_bit_set(i32 %x) {
 ; CHECK-LABEL: @msb_at_14_bit_set(
-; CHECK-NEXT:    [[C:%.*]] = call range(i32 0, 33) i32 @llvm.ctlz.i32(i32 [[X:%.*]], i1 true)
-; CHECK-NEXT:    [[XORED:%.*]] = xor i32 [[C]], 31
-; CHECK-NEXT:    [[SHAMT:%.*]] = sub nsw i32 14, [[XORED]]
-; CHECK-NEXT:    [[S:%.*]] = shl i32 [[X]], [[SHAMT]]
-; CHECK-NEXT:    [[R:%.*]] = and i32 [[S]], 16384
-; CHECK-NEXT:    ret i32 [[R]]
+; CHECK-NEXT:    ret i32 16384
 ;
   %c = call i32 @llvm.ctlz.i32(i32 %x, i1 true)
   %xored = xor i32 %c, 31
@@ -29,12 +24,7 @@ define i32 @msb_at_14_bit_set(i32 %x) {
 
 define i32 @msb_at_14_high_bits_clear(i32 %x) {
 ; CHECK-LABEL: @msb_at_14_high_bits_clear(
-; CHECK-NEXT:    [[C:%.*]] = call range(i32 0, 33) i32 @llvm.ctlz.i32(i32 [[X:%.*]], i1 true)
-; CHECK-NEXT:    [[XORED:%.*]] = xor i32 [[C]], 31
-; CHECK-NEXT:    [[SHAMT:%.*]] = sub nsw i32 14, [[XORED]]
-; CHECK-NEXT:    [[S:%.*]] = shl i32 [[X]], [[SHAMT]]
-; CHECK-NEXT:    [[R:%.*]] = and i32 [[S]], -32768
-; CHECK-NEXT:    ret i32 [[R]]
+; CHECK-NEXT:    ret i32 0
 ;
   %c = call i32 @llvm.ctlz.i32(i32 %x, i1 true)
   %xored = xor i32 %c, 31
@@ -46,12 +36,7 @@ define i32 @msb_at_14_high_bits_clear(i32 %x) {
 
 define i32 @msb_at_14_xor_commuted(i32 %x) {
 ; CHECK-LABEL: @msb_at_14_xor_commuted(
-; CHECK-NEXT:    [[C:%.*]] = call range(i32 0, 33) i32 @llvm.ctlz.i32(i32 [[X:%.*]], i1 true)
-; CHECK-NEXT:    [[XORED:%.*]] = xor i32 [[C]], 31
-; CHECK-NEXT:    [[SHAMT:%.*]] = sub nsw i32 14, [[XORED]]
-; CHECK-NEXT:    [[S:%.*]] = shl i32 [[X]], [[SHAMT]]
-; CHECK-NEXT:    [[R:%.*]] = and i32 [[S]], -32768
-; CHECK-NEXT:    ret i32 [[R]]
+; CHECK-NEXT:    ret i32 0
 ;
   %c = call i32 @llvm.ctlz.i32(i32 %x, i1 true)
   %xored = xor i32 31, %c
@@ -63,12 +48,7 @@ define i32 @msb_at_14_xor_commuted(i32 %x) {
 
 define i32 @msb_at_14_zero_not_undef(i32 %x) {
 ; CHECK-LABEL: @msb_at_14_zero_not_undef(
-; CHECK-NEXT:    [[C:%.*]] = call range(i32 0, 33) i32 @llvm.ctlz.i32(i32 [[X:%.*]], i1 false)
-; CHECK-NEXT:    [[XORED:%.*]] = xor i32 [[C]], 31
-; CHECK-NEXT:    [[SHAMT:%.*]] = sub nsw i32 14, [[XORED]]
-; CHECK-NEXT:    [[S:%.*]] = shl i32 [[X]], [[SHAMT]]
-; CHECK-NEXT:    [[R:%.*]] = and i32 [[S]], 16384
-; CHECK-NEXT:    ret i32 [[R]]
+; CHECK-NEXT:    ret i32 16384
 ;
   %c = call i32 @llvm.ctlz.i32(i32 %x, i1 false)
   %xored = xor i32 %c, 31
@@ -80,12 +60,7 @@ define i32 @msb_at_14_zero_not_undef(i32 %x) {
 
 define <2 x i32> @msb_at_14_vec(<2 x i32> %x) {
 ; CHECK-LABEL: @msb_at_14_vec(
-; CHECK-NEXT:    [[C:%.*]] = call range(i32 0, 33) <2 x i32> @llvm.ctlz.v2i32(<2 x i32> [[X:%.*]], i1 true)
-; CHECK-NEXT:    [[XORED:%.*]] = xor <2 x i32> [[C]], splat (i32 31)
-; CHECK-NEXT:    [[SHAMT:%.*]] = sub nsw <2 x i32> splat (i32 14), [[XORED]]
-; CHECK-NEXT:    [[S:%.*]] = shl <2 x i32> [[X]], [[SHAMT]]
-; CHECK-NEXT:    [[R:%.*]] = and <2 x i32> [[S]], splat (i32 16384)
-; CHECK-NEXT:    ret <2 x i32> [[R]]
+; CHECK-NEXT:    ret <2 x i32> splat (i32 16384)
 ;
   %c = call <2 x i32> @llvm.ctlz.v2i32(<2 x i32> %x, i1 true)
   %xored = xor <2 x i32> %c, <i32 31, i32 31>

>From d288b52eb53e4d378b454602f3244be0e2b2d8b7 Mon Sep 17 00:00:00 2001
From: Marina Taylor <marina_taylor at apple.com>
Date: Thu, 28 May 2026 16:52:35 +0100
Subject: [PATCH 3/3] Review fixes

---
 llvm/lib/Analysis/ValueTracking.cpp           |  8 ++---
 .../ValueTracking/knownbits-shl-ctlz-msb.ll   | 29 -------------------
 2 files changed, 4 insertions(+), 33 deletions(-)

diff --git a/llvm/lib/Analysis/ValueTracking.cpp b/llvm/lib/Analysis/ValueTracking.cpp
index a5643a14bf435..82e4108ad9bff 100644
--- a/llvm/lib/Analysis/ValueTracking.cpp
+++ b/llvm/lib/Analysis/ValueTracking.cpp
@@ -1634,14 +1634,14 @@ static void computeKnownBitsFromOperator(const Operator *I,
     if (match(I->getOperand(0), m_APInt(C)))
       Known.Zero.setLowBits(C->countr_zero());
 
-    // shl X, sub(Y, xor(ctlz(X), BitWidth-1)) shifts X so that its MSB lands
-    // at bit Y, when BitWidth is a power of 2.
+    // shl X, sub(Y, xor(ctlz(X, true), BitWidth-1)) shifts X so that its MSB
+    // lands at bit Y, when BitWidth is a power of 2.
     const APInt *YC;
     Value *X = I->getOperand(0);
     if (isPowerOf2_32(BitWidth) &&
         match(I->getOperand(1),
-              m_Sub(m_APInt(YC), m_c_Xor(m_Ctlz(m_Specific(X), m_Value()),
-                                         m_SpecificInt(BitWidth - 1)))) &&
+              m_Sub(m_APInt(YC), m_Xor(m_Ctlz(m_Specific(X), m_One()),
+                                       m_SpecificInt(BitWidth - 1)))) &&
         YC->ult(BitWidth - 1)) {
       unsigned Y = YC->getZExtValue();
       Known.One.setBit(Y);
diff --git a/llvm/test/Analysis/ValueTracking/knownbits-shl-ctlz-msb.ll b/llvm/test/Analysis/ValueTracking/knownbits-shl-ctlz-msb.ll
index 9a101d945c1ed..ba71154cc05f8 100644
--- a/llvm/test/Analysis/ValueTracking/knownbits-shl-ctlz-msb.ll
+++ b/llvm/test/Analysis/ValueTracking/knownbits-shl-ctlz-msb.ll
@@ -5,11 +5,6 @@
 ; When BitWidth is a power of 2 and Y < BitWidth-1, the result has its MSB
 ; at bit Y: bit Y is set and bits above Y are clear.
 
-declare i16 @llvm.ctlz.i16(i16, i1 immarg)
-declare i31 @llvm.ctlz.i31(i31, i1 immarg)
-declare i32 @llvm.ctlz.i32(i32, i1 immarg)
-declare i64 @llvm.ctlz.i64(i64, i1 immarg)
-
 define i32 @msb_at_14_bit_set(i32 %x) {
 ; CHECK-LABEL: @msb_at_14_bit_set(
 ; CHECK-NEXT:    ret i32 16384
@@ -34,30 +29,6 @@ define i32 @msb_at_14_high_bits_clear(i32 %x) {
   ret i32 %r
 }
 
-define i32 @msb_at_14_xor_commuted(i32 %x) {
-; CHECK-LABEL: @msb_at_14_xor_commuted(
-; CHECK-NEXT:    ret i32 0
-;
-  %c = call i32 @llvm.ctlz.i32(i32 %x, i1 true)
-  %xored = xor i32 31, %c
-  %shamt = sub i32 14, %xored
-  %s = shl i32 %x, %shamt
-  %r = and i32 %s, 4294934528
-  ret i32 %r
-}
-
-define i32 @msb_at_14_zero_not_undef(i32 %x) {
-; CHECK-LABEL: @msb_at_14_zero_not_undef(
-; CHECK-NEXT:    ret i32 16384
-;
-  %c = call i32 @llvm.ctlz.i32(i32 %x, i1 false)
-  %xored = xor i32 %c, 31
-  %shamt = sub i32 14, %xored
-  %s = shl i32 %x, %shamt
-  %r = and i32 %s, 16384
-  ret i32 %r
-}
-
 define <2 x i32> @msb_at_14_vec(<2 x i32> %x) {
 ; CHECK-LABEL: @msb_at_14_vec(
 ; CHECK-NEXT:    ret <2 x i32> splat (i32 16384)



More information about the llvm-commits mailing list