[llvm] [InstCombine] Evaluate zext nneg as sext where possible (PR #212230)

John Brawn via llvm-commits llvm-commits at lists.llvm.org
Wed Aug 12 06:47:33 PDT 2026


https://github.com/john-brawn-arm updated https://github.com/llvm/llvm-project/pull/212230

>From 873ac901b63e0fcfde2cbf551f8539dda5d2934d Mon Sep 17 00:00:00 2001
From: John Brawn <john.brawn at arm.com>
Date: Mon, 20 Jul 2026 11:14:27 +0100
Subject: [PATCH 1/7] [InstCombine] Evaluate zext nneg as sext where possible

We can convert zext nneg to sext, and doing so when evaluating an
expression in a wider type means any negative constant operands are
sign-extended instead of zero-extended, meaning the high bits of the
result will be correct without needing to mask them. We do however
need to be careful of signed wrapping, as that can result in a
positive result in the narrower type which becomes negative when
sign-extended.
---
 .../InstCombine/InstCombineCasts.cpp          |  72 ++++--
 llvm/test/Transforms/InstCombine/zext-nneg.ll | 233 ++++++++++++++++++
 2 files changed, 285 insertions(+), 20 deletions(-)
 create mode 100644 llvm/test/Transforms/InstCombine/zext-nneg.ll

diff --git a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
index 2db6396b9d661..fb40c656dc4ae 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
@@ -318,8 +318,10 @@ class TypeEvaluationHelper {
 
   /// Return true if we can take the specified value and return it as type Ty
   /// without inserting any new casts and without changing the value of the
-  /// common low bits.
-  [[nodiscard]] static bool canEvaluateSExtd(Value *V, Type *Ty);
+  /// common low bits. If NoSignedWrap is true then the calculation of V must
+  /// not involve arithmetic that could performed signed wrapping.
+  [[nodiscard]] static bool canEvaluateSExtd(Value *V, Type *Ty,
+                                             bool NoSignedWrap = false);
 
 private:
   /// Constants and extensions/truncates from the destination type are always
@@ -458,8 +460,10 @@ class TypeEvaluationHelper {
                                           unsigned &BitsToClear,
                                           InstCombinerImpl &IC,
                                           Instruction *CxtI);
-  [[nodiscard]] bool canEvaluateSExtdImpl(Value *V, Type *Ty);
-  [[nodiscard]] bool canEvaluateSExtdPred(Value *V, Type *Ty);
+  [[nodiscard]] bool canEvaluateSExtdImpl(Value *V, Type *Ty,
+                                          bool NoSignedWrap);
+  [[nodiscard]] bool canEvaluateSExtdPred(Value *V, Type *Ty,
+                                          bool NoSignedWrap);
 
   /// A bookkeeping map to memorize an already made decision for a traversed
   /// value.
@@ -1615,7 +1619,18 @@ Instruction *InstCombinerImpl::visitZExt(ZExtInst &Zext) {
         dbgs() << "ICE: EvaluateInDifferentType converting expression type"
                   " to avoid zero extend: "
                << Zext << '\n');
-    Value *Res = EvaluateInDifferentType(Src, DestTy, false);
+
+    // zext nneg means Src is non-negative and we can treat this as an sext, and
+    // if no signed wrapping occurs we know that Src evaluated as a signed
+    // DestTy will also be non-negative. Evaluating as a signed type means that
+    // any constant operands will be sign-extended instead of zero-extended,
+    // meaning they remain in the range of a signed SrcTy so we won't need to
+    // clear any high bits.
+    bool EvaluateAsSigned =
+        Zext.hasNonNeg() &&
+        TypeEvaluationHelper::canEvaluateSExtd(Src, DestTy, true);
+
+    Value *Res = EvaluateInDifferentType(Src, DestTy, EvaluateAsSigned);
     assert(Res->getType() == DestTy);
 
     // Preserve debug values referring to Src if the zext is its last use.
@@ -1627,10 +1642,14 @@ Instruction *InstCombinerImpl::visitZExt(ZExtInst &Zext) {
     uint32_t DestBitSize = DestTy->getScalarSizeInBits();
 
     // If the high bits are already filled with zeros, just replace this
-    // cast with the result.
-    if (MaskedValueIsZero(
-            Res, APInt::getHighBitsSet(DestBitSize, DestBitSize - SrcBitsKept),
-            &Zext))
+    // cast with the result. If we've evaluated as a signed expressions then
+    // instead check that the high bits are the sign bit, which we know is zero.
+    if (EvaluateAsSigned
+            ? (ComputeNumSignBits(Res, &Zext) > DestBitSize - SrcBitsKept)
+            : MaskedValueIsZero(
+                  Res,
+                  APInt::getHighBitsSet(DestBitSize, DestBitSize - SrcBitsKept),
+                  &Zext))
       return replaceInstUsesWith(Zext, Res);
 
     // We need to emit an AND to clear the high bits.
@@ -1834,22 +1853,35 @@ Instruction *InstCombinerImpl::transformSExtICmp(ICmpInst *Cmp,
 ///
 /// This function works on both vectors and scalars.
 ///
-bool TypeEvaluationHelper::canEvaluateSExtd(Value *V, Type *Ty) {
+bool TypeEvaluationHelper::canEvaluateSExtd(Value *V, Type *Ty,
+                                            bool NoSignedWrap) {
   TypeEvaluationHelper TYH;
-  return TYH.canEvaluateSExtdImpl(V, Ty) && TYH.allPendingVisited();
+  return TYH.canEvaluateSExtdImpl(V, Ty, NoSignedWrap) &&
+         TYH.allPendingVisited();
 }
 
-bool TypeEvaluationHelper::canEvaluateSExtdImpl(Value *V, Type *Ty) {
-  return canEvaluate(V, Ty, [this](Value *V, Type *Ty) {
-    return canEvaluateSExtdPred(V, Ty);
+bool TypeEvaluationHelper::canEvaluateSExtdImpl(Value *V, Type *Ty,
+                                                bool NoSignedWrap) {
+  return canEvaluate(V, Ty, [this, NoSignedWrap](Value *V, Type *Ty) {
+    return canEvaluateSExtdPred(V, Ty, NoSignedWrap);
   });
 }
 
-bool TypeEvaluationHelper::canEvaluateSExtdPred(Value *V, Type *Ty) {
+bool TypeEvaluationHelper::canEvaluateSExtdPred(Value *V, Type *Ty,
+                                                bool NoSignedWrap) {
   assert(V->getType()->getScalarSizeInBits() < Ty->getScalarSizeInBits() &&
          "Can't sign extend type to a smaller type");
 
   auto *I = cast<Instruction>(V);
+
+  // If signed wrapping is forbidden then check that all instructions that could
+  // potentially wrap have the nsw flag.
+  if (NoSignedWrap &&
+      (isa<OverflowingBinaryOperator>(I) ||
+       I->getOpcode() == Instruction::Trunc) &&
+      !I->hasNoSignedWrap())
+    return false;
+
   switch (I->getOpcode()) {
   case Instruction::SExt:  // sext(sext(x)) -> sext(x)
   case Instruction::ZExt:  // sext(zext(x)) -> zext(x)
@@ -1862,15 +1894,15 @@ bool TypeEvaluationHelper::canEvaluateSExtdPred(Value *V, Type *Ty) {
   case Instruction::Sub:
   case Instruction::Mul:
     // These operators can all arbitrarily be extended if their inputs can.
-    return canEvaluateSExtdImpl(I->getOperand(0), Ty) &&
-           canEvaluateSExtdImpl(I->getOperand(1), Ty);
+    return canEvaluateSExtdImpl(I->getOperand(0), Ty, NoSignedWrap) &&
+           canEvaluateSExtdImpl(I->getOperand(1), Ty, NoSignedWrap);
 
     // case Instruction::Shl:   TODO
     // case Instruction::LShr:  TODO
 
   case Instruction::Select:
-    return canEvaluateSExtdImpl(I->getOperand(1), Ty) &&
-           canEvaluateSExtdImpl(I->getOperand(2), Ty);
+    return canEvaluateSExtdImpl(I->getOperand(1), Ty, NoSignedWrap) &&
+           canEvaluateSExtdImpl(I->getOperand(2), Ty, NoSignedWrap);
 
   case Instruction::PHI: {
     // We can change a phi if we can change all operands.  Note that we never
@@ -1878,7 +1910,7 @@ bool TypeEvaluationHelper::canEvaluateSExtdPred(Value *V, Type *Ty) {
     // chain loops.
     PHINode *PN = cast<PHINode>(I);
     for (Value *IncValue : PN->incoming_values())
-      if (!canEvaluateSExtdImpl(IncValue, Ty))
+      if (!canEvaluateSExtdImpl(IncValue, Ty, NoSignedWrap))
         return false;
     return true;
   }
diff --git a/llvm/test/Transforms/InstCombine/zext-nneg.ll b/llvm/test/Transforms/InstCombine/zext-nneg.ll
new file mode 100644
index 0000000000000..bb75d37a7824f
--- /dev/null
+++ b/llvm/test/Transforms/InstCombine/zext-nneg.ll
@@ -0,0 +1,233 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; RUN: opt < %s -passes=instcombine -S | FileCheck %s
+
+target datalayout = "n64"
+
+; When evaluated as i64 we need to restrict the range of the output to that of
+; i32 using an and, as negative values of the i32 add become positive values
+; when zero-extended.
+define i64 @zext_of_sext_nsw(i16 %arg) {
+; CHECK-LABEL: define i64 @zext_of_sext_nsw(
+; CHECK-SAME: i16 [[ARG:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[SEXT:%.*]] = sext i16 [[ARG]] to i64
+; CHECK-NEXT:    [[ADD:%.*]] = add nsw i64 [[SEXT]], 4294967295
+; CHECK-NEXT:    [[ZEXT:%.*]] = and i64 [[ADD]], 4294967295
+; CHECK-NEXT:    ret i64 [[ZEXT]]
+;
+entry:
+  %sext = sext i16 %arg to i32
+  %add = add nsw i32 %sext, -1
+  %zext = zext i32 %add to i64
+  ret i64 %zext
+}
+
+define i64 @zext_of_zext_nsw(i16 %arg) {
+; CHECK-LABEL: define i64 @zext_of_zext_nsw(
+; CHECK-SAME: i16 [[ARG:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[SEXT:%.*]] = zext i16 [[ARG]] to i64
+; CHECK-NEXT:    [[ADD:%.*]] = add nuw nsw i64 [[SEXT]], 4294967295
+; CHECK-NEXT:    [[ZEXT:%.*]] = and i64 [[ADD]], 4294967295
+; CHECK-NEXT:    ret i64 [[ZEXT]]
+;
+entry:
+  %sext = zext i16 %arg to i32
+  %add = add nsw i32 %sext, -1
+  %zext = zext i32 %add to i64
+  ret i64 %zext
+}
+
+; When the zext is nneg we know the add result can never be negative, and nsw on
+; the add means evaluating the add at a wider width can't result in the sign of
+; the result changing, so we don't need to restrict to the range of an i32.
+define i64 @zext_nneg_of_sext_nsw(i16 %arg) {
+; CHECK-LABEL: define i64 @zext_nneg_of_sext_nsw(
+; CHECK-SAME: i16 [[ARG:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[SEXT:%.*]] = sext i16 [[ARG]] to i64
+; CHECK-NEXT:    [[ADD:%.*]] = add nsw i64 [[SEXT]], -1
+; CHECK-NEXT:    ret i64 [[ADD]]
+;
+entry:
+  %sext = sext i16 %arg to i32
+  %add = add nsw i32 %sext, -1
+  %zext = zext nneg i32 %add to i64
+  ret i64 %zext
+}
+
+define i64 @zext_nneg_of_zext_nsw(i16 %arg) {
+; CHECK-LABEL: define i64 @zext_nneg_of_zext_nsw(
+; CHECK-SAME: i16 [[ARG:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[SEXT:%.*]] = zext i16 [[ARG]] to i64
+; CHECK-NEXT:    [[ADD:%.*]] = add nsw i64 [[SEXT]], -1
+; CHECK-NEXT:    ret i64 [[ADD]]
+;
+entry:
+  %sext = zext i16 %arg to i32
+  %add = add nsw i32 %sext, -1
+  %zext = zext nneg i32 %add to i64
+  ret i64 %zext
+}
+
+; We can still avoid an and even without nsw on the add as -1 is small enough
+; that we know there are no sign or zero extended values that could result in
+; the output wrapping.
+define i64 @zext_nneg_of_sext(i16 %arg) {
+; CHECK-LABEL: define i64 @zext_nneg_of_sext(
+; CHECK-SAME: i16 [[ARG:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[SEXT:%.*]] = sext i16 [[ARG]] to i64
+; CHECK-NEXT:    [[ADD:%.*]] = add nsw i64 [[SEXT]], -1
+; CHECK-NEXT:    ret i64 [[ADD]]
+;
+entry:
+  %sext = sext i16 %arg to i32
+  %add = add i32 %sext, -1
+  %zext = zext nneg i32 %add to i64
+  ret i64 %zext
+}
+
+define i64 @zext_nneg_of_zext(i16 %arg) {
+; CHECK-LABEL: define i64 @zext_nneg_of_zext(
+; CHECK-SAME: i16 [[ARG:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[SEXT:%.*]] = zext i16 [[ARG]] to i64
+; CHECK-NEXT:    [[ADD:%.*]] = add nsw i64 [[SEXT]], -1
+; CHECK-NEXT:    ret i64 [[ADD]]
+;
+entry:
+  %sext = zext i16 %arg to i32
+  %add = add i32 %sext, -1
+  %zext = zext nneg i32 %add to i64
+  ret i64 %zext
+}
+
+; -2147450881 is (INT32_MIN - INT16_MIN - 1), so if %arg is INT16_MIN the result
+; is INT32_MIN-1, so when done in i32 precision will wrap around to INT32_MAX.
+; We therefore need to use an and to restrict the result to the range of an i32.
+; Theoretically we could optimize this to just return INT32_MIN-1, as all other
+; %add values are negative and thus result in poison when used as an argument to
+; zext nneg.
+define i64 @zext_nneg_of_sext_large_negative(i16 %arg) {
+; CHECK-LABEL: define i64 @zext_nneg_of_sext_large_negative(
+; CHECK-SAME: i16 [[ARG:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[SEXT:%.*]] = sext i16 [[ARG]] to i64
+; CHECK-NEXT:    [[ADD:%.*]] = add nsw i64 [[SEXT]], 2147516415
+; CHECK-NEXT:    [[ZEXT:%.*]] = and i64 [[ADD]], 4294967295
+; CHECK-NEXT:    ret i64 [[ZEXT]]
+;
+entry:
+  %sext = sext i16 %arg to i32
+  %add = add i32 %sext, -2147450881
+  %zext = zext nneg i32 %add to i64
+  ret i64 %zext
+}
+
+; We don't need to worry about wrapping, and so never need to restrict the range
+; of the output, for zext of zext, as the result of the zext is, when
+; interpreted as a signed integer, always positive.
+; Theoretically here we could optimize this to return poison, as the result of
+; the %add is always negative ans thus gives poison when passed to zext nneg.
+define i64 @zext_nneg_of_zext_large_negative(i16 %arg) {
+; CHECK-LABEL: define i64 @zext_nneg_of_zext_large_negative(
+; CHECK-SAME: i16 [[ARG:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[SEXT:%.*]] = zext i16 [[ARG]] to i64
+; CHECK-NEXT:    [[ADD:%.*]] = add nuw nsw i64 [[SEXT]], 2147516415
+; CHECK-NEXT:    ret i64 [[ADD]]
+;
+entry:
+  %sext = zext i16 %arg to i32
+  %add = add i32 %sext, -2147450881
+  %zext = zext nneg i32 %add to i64
+  ret i64 %zext
+}
+
+; 4294934529 is (UINT32_MAX - INT16_MAX + 1), so if %arg is INT16_MAX the result
+; is UINT32_MAX+1, causing the result to wrap around to zero. However 4294934529
+; interpreted as a signed i32 is -32767, so adding that at i64 width will give
+; the same result without needing to care about unsigned wrapping.
+define i64 @zext_nneg_of_sext_large_positive(i16 %arg) {
+; CHECK-LABEL: define i64 @zext_nneg_of_sext_large_positive(
+; CHECK-SAME: i16 [[ARG:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[SEXT:%.*]] = sext i16 [[ARG]] to i64
+; CHECK-NEXT:    [[ADD:%.*]] = add nsw i64 [[SEXT]], -32767
+; CHECK-NEXT:    ret i64 [[ADD]]
+;
+entry:
+  %sext = sext i16 %arg to i32
+  %add = add i32 %sext, 4294934529
+  %zext = zext nneg i32 %add to i64
+  ret i64 %zext
+}
+
+define i64 @zext_nneg_of_zext_large_positive(i16 %arg) {
+; CHECK-LABEL: define i64 @zext_nneg_of_zext_large_positive(
+; CHECK-SAME: i16 [[ARG:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[SEXT:%.*]] = zext i16 [[ARG]] to i64
+; CHECK-NEXT:    [[ADD:%.*]] = add nsw i64 [[SEXT]], -32767
+; CHECK-NEXT:    ret i64 [[ADD]]
+;
+entry:
+  %sext = zext i16 %arg to i32
+  %add = add i32 %sext, 4294934529
+  %zext = zext nneg i32 %add to i64
+  ret i64 %zext
+}
+
+; 2271560481 is 0x87654321. Sign-extending it would be a problem if the zext
+; weren't nneg, but because it is we know that the sign-extended bits of arg
+; must have the same value and cancel out to zero.
+define i64 @zext_of_sext_xor(i16 %arg) {
+; CHECK-LABEL: define i64 @zext_of_sext_xor(
+; CHECK-SAME: i16 [[ARG:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[SEXT:%.*]] = sext i16 [[ARG]] to i64
+; CHECK-NEXT:    [[XOR:%.*]] = xor i64 [[SEXT]], -2023406815
+; CHECK-NEXT:    ret i64 [[XOR]]
+;
+entry:
+  %sext = sext i16 %arg to i32
+  %xor = xor i32 %sext, 2271560481
+  %zext = zext nneg i32 %xor to i64
+  ret i64 %zext
+}
+
+; Theoretically this can be replaced with ret poison, as the or means that it's
+; guaranteed that the sign bit is set which isn't permitted by the zext nneg.
+define i64 @zext_of_sext_or(i16 %arg) {
+; CHECK-LABEL: define i64 @zext_of_sext_or(
+; CHECK-SAME: i16 [[ARG:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[SEXT:%.*]] = sext i16 [[ARG]] to i64
+; CHECK-NEXT:    [[OR:%.*]] = or i64 [[SEXT]], -2023406815
+; CHECK-NEXT:    ret i64 [[OR]]
+;
+entry:
+  %sext = sext i16 %arg to i32
+  %or = or i32 %sext, 2271560481
+  %zext = zext nneg i32 %or to i64
+  ret i64 %zext
+}
+
+; The and preserves the sign bit of the sext'd arg, so we know it must be zero
+; from the nneg, so it's safe to sign-extend the and immediate.
+define i64 @zext_of_sext_and(i16 %arg) {
+; CHECK-LABEL: define i64 @zext_of_sext_and(
+; CHECK-SAME: i16 [[ARG:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[SEXT:%.*]] = sext i16 [[ARG]] to i64
+; CHECK-NEXT:    [[AND:%.*]] = and i64 [[SEXT]], -2023406815
+; CHECK-NEXT:    ret i64 [[AND]]
+;
+entry:
+  %sext = sext i16 %arg to i32
+  %and = and i32 %sext, 2271560481
+  %zext = zext nneg i32 %and to i64
+  ret i64 %zext
+}

>From 3ced1f69cb0502239950e4549f9d629f66932212 Mon Sep 17 00:00:00 2001
From: John Brawn <john.brawn at arm.com>
Date: Thu, 6 Aug 2026 15:06:43 +0100
Subject: [PATCH 2/7] Don't call canEvaluateZExtd if CanEvaluateSigned is true

---
 .../InstCombine/InstCombineCasts.cpp          | 26 ++++++------
 .../Transforms/InstCombine/cast-mul-select.ll | 40 ++++++++-----------
 2 files changed, 30 insertions(+), 36 deletions(-)

diff --git a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
index fb40c656dc4ae..2d134c8458882 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
@@ -1606,11 +1606,21 @@ Instruction *InstCombinerImpl::visitZExt(ZExtInst &Zext) {
   if (SrcTy->isIntOrIntVectorTy(1) && Zext.hasNonNeg())
     return replaceInstUsesWith(Zext, Constant::getNullValue(Zext.getType()));
 
+  // zext nneg means Src is non-negative and we can treat this as an sext, and
+  // if no signed wrapping occurs we know that Src evaluated as a signed
+  // DestTy will also be non-negative. Evaluating as a signed type means that
+  // any constant operands will be sign-extended instead of zero-extended,
+  // meaning they remain in the range of a signed SrcTy so we won't need to
+  // clear any high bits.
+  bool EvaluateAsSigned =
+      Zext.hasNonNeg() &&
+      TypeEvaluationHelper::canEvaluateSExtd(Src, DestTy, true);
+
   // Try to extend the entire expression tree to the wide destination type.
-  unsigned BitsToClear;
+  unsigned BitsToClear = 0;
   if (shouldChangeType(SrcTy, DestTy) &&
-      TypeEvaluationHelper::canEvaluateZExtd(Src, DestTy, BitsToClear, *this,
-                                             &Zext)) {
+      (EvaluateAsSigned || TypeEvaluationHelper::canEvaluateZExtd(
+                               Src, DestTy, BitsToClear, *this, &Zext))) {
     assert(BitsToClear <= SrcTy->getScalarSizeInBits() &&
            "Can't clear more bits than in SrcTy");
 
@@ -1620,16 +1630,6 @@ Instruction *InstCombinerImpl::visitZExt(ZExtInst &Zext) {
                   " to avoid zero extend: "
                << Zext << '\n');
 
-    // zext nneg means Src is non-negative and we can treat this as an sext, and
-    // if no signed wrapping occurs we know that Src evaluated as a signed
-    // DestTy will also be non-negative. Evaluating as a signed type means that
-    // any constant operands will be sign-extended instead of zero-extended,
-    // meaning they remain in the range of a signed SrcTy so we won't need to
-    // clear any high bits.
-    bool EvaluateAsSigned =
-        Zext.hasNonNeg() &&
-        TypeEvaluationHelper::canEvaluateSExtd(Src, DestTy, true);
-
     Value *Res = EvaluateInDifferentType(Src, DestTy, EvaluateAsSigned);
     assert(Res->getType() == DestTy);
 
diff --git a/llvm/test/Transforms/InstCombine/cast-mul-select.ll b/llvm/test/Transforms/InstCombine/cast-mul-select.ll
index 29c5bb57a4667..1a0728778cc34 100644
--- a/llvm/test/Transforms/InstCombine/cast-mul-select.ll
+++ b/llvm/test/Transforms/InstCombine/cast-mul-select.ll
@@ -113,22 +113,18 @@ define i32 @eval_trunc_multi_use_in_one_inst(i32 %x) {
 
 define i32 @eval_zext_multi_use_in_one_inst(i32 %x) {
 ; CHECK-LABEL: @eval_zext_multi_use_in_one_inst(
-; CHECK-NEXT:    [[T:%.*]] = trunc i32 [[X:%.*]] to i16
-; CHECK-NEXT:    [[A:%.*]] = and i16 [[T]], 5
-; CHECK-NEXT:    [[M:%.*]] = mul nuw nsw i16 [[A]], [[A]]
-; CHECK-NEXT:    [[R:%.*]] = zext nneg i16 [[M]] to i32
+; CHECK-NEXT:    [[A:%.*]] = and i32 [[X:%.*]], 5
+; CHECK-NEXT:    [[R:%.*]] = mul nuw nsw i32 [[A]], [[A]]
 ; CHECK-NEXT:    ret i32 [[R]]
 ;
 ; DBGINFO-LABEL: @eval_zext_multi_use_in_one_inst(
-; DBGINFO-NEXT:    [[T:%.*]] = trunc i32 [[X:%.*]] to i16, !dbg [[DBG69:![0-9]+]]
-; DBGINFO-NEXT:      #dbg_value(i16 [[T]], [[META64:![0-9]+]], !DIExpression(), [[DBG69]])
-; DBGINFO-NEXT:    [[A:%.*]] = and i16 [[T]], 5, !dbg [[DBG70:![0-9]+]]
-; DBGINFO-NEXT:      #dbg_value(i16 [[A]], [[META66:![0-9]+]], !DIExpression(), [[DBG70]])
-; DBGINFO-NEXT:    [[M:%.*]] = mul nuw nsw i16 [[A]], [[A]], !dbg [[DBG71:![0-9]+]]
-; DBGINFO-NEXT:      #dbg_value(i16 [[M]], [[META67:![0-9]+]], !DIExpression(), [[DBG71]])
-; DBGINFO-NEXT:    [[R:%.*]] = zext nneg i16 [[M]] to i32, !dbg [[DBG72:![0-9]+]]
-; DBGINFO-NEXT:      #dbg_value(i32 [[R]], [[META68:![0-9]+]], !DIExpression(), [[DBG72]])
-; DBGINFO-NEXT:    ret i32 [[R]], !dbg [[DBG73:![0-9]+]]
+; DBGINFO-NEXT:      #dbg_value(i32 [[X:%.*]], [[META64:![0-9]+]], !DIExpression(DW_OP_LLVM_convert, 32, DW_ATE_unsigned, DW_OP_LLVM_convert, 16, DW_ATE_unsigned, DW_OP_stack_value), [[META69:![0-9]+]])
+; DBGINFO-NEXT:    [[A:%.*]] = and i32 [[X]], 5, !dbg [[DBG70:![0-9]+]]
+; DBGINFO-NEXT:      #dbg_value(i32 [[X]], [[META66:![0-9]+]], !DIExpression(DW_OP_LLVM_convert, 32, DW_ATE_unsigned, DW_OP_LLVM_convert, 16, DW_ATE_unsigned, DW_OP_constu, 5, DW_OP_and, DW_OP_stack_value), [[DBG70]])
+; DBGINFO-NEXT:    [[M:%.*]] = mul nuw nsw i32 [[A]], [[A]], !dbg [[DBG71:![0-9]+]]
+; DBGINFO-NEXT:      #dbg_value(i32 [[M]], [[META67:![0-9]+]], !DIExpression(), [[DBG71]])
+; DBGINFO-NEXT:      #dbg_value(i32 [[M]], [[META68:![0-9]+]], !DIExpression(), [[META72:![0-9]+]])
+; DBGINFO-NEXT:    ret i32 [[M]], !dbg [[DBG73:![0-9]+]]
 ;
   %t = trunc i32 %x to i16
   %a = and i16 %t, 5
@@ -174,7 +170,7 @@ define void @PR36225(i32 %a, i32 %b, i1 %c1, i3 %v1, i3 %v2) {
 ; CHECK-NEXT:    br i1 [[C1:%.*]], label [[FOR_BODY3_US:%.*]], label [[FOR_BODY3:%.*]]
 ; CHECK:       for.body3.us:
 ; CHECK-NEXT:    [[TOBOOL:%.*]] = icmp eq i32 [[B:%.*]], 0
-; CHECK-NEXT:    [[SPEC_SELECT:%.*]] = select i1 [[TOBOOL]], i8 0, i8 4
+; CHECK-NEXT:    [[SPEC_SELECT:%.*]] = select i1 [[TOBOOL]], i32 0, i32 4
 ; CHECK-NEXT:    switch i3 [[V1:%.*]], label [[EXIT:%.*]] [
 ; CHECK-NEXT:      i3 0, label [[FOR_END:%.*]]
 ; CHECK-NEXT:      i3 -1, label [[FOR_END]]
@@ -185,8 +181,7 @@ define void @PR36225(i32 %a, i32 %b, i1 %c1, i3 %v1, i3 %v2) {
 ; CHECK-NEXT:      i3 -1, label [[FOR_END]]
 ; CHECK-NEXT:    ]
 ; CHECK:       for.end:
-; CHECK-NEXT:    [[H:%.*]] = phi i8 [ [[SPEC_SELECT]], [[FOR_BODY3_US]] ], [ [[SPEC_SELECT]], [[FOR_BODY3_US]] ], [ 0, [[FOR_BODY3]] ], [ 0, [[FOR_BODY3]] ]
-; CHECK-NEXT:    [[CONV:%.*]] = zext nneg i8 [[H]] to i32
+; CHECK-NEXT:    [[CONV:%.*]] = phi i32 [ [[SPEC_SELECT]], [[FOR_BODY3_US]] ], [ [[SPEC_SELECT]], [[FOR_BODY3_US]] ], [ 0, [[FOR_BODY3]] ], [ 0, [[FOR_BODY3]] ]
 ; CHECK-NEXT:    [[CMP:%.*]] = icmp sgt i32 [[A:%.*]], [[CONV]]
 ; CHECK-NEXT:    br i1 [[CMP]], label [[EXIT]], label [[EXIT2:%.*]]
 ; CHECK:       exit2:
@@ -203,8 +198,8 @@ define void @PR36225(i32 %a, i32 %b, i1 %c1, i3 %v1, i3 %v2) {
 ; DBGINFO:       for.body3.us:
 ; DBGINFO-NEXT:    [[TOBOOL:%.*]] = icmp eq i32 [[B]], 0, !dbg [[META95]]
 ; DBGINFO-NEXT:      #dbg_value(i1 [[TOBOOL]], [[META89]], !DIExpression(), [[META95]])
-; DBGINFO-NEXT:    [[SPEC_SELECT:%.*]] = select i1 [[TOBOOL]], i8 0, i8 4, !dbg [[DBG97:![0-9]+]]
-; DBGINFO-NEXT:      #dbg_value(i8 [[SPEC_SELECT]], [[META90:![0-9]+]], !DIExpression(), [[DBG97]])
+; DBGINFO-NEXT:    [[SPEC_SELECT:%.*]] = select i1 [[TOBOOL]], i32 0, i32 4, !dbg [[DBG97:![0-9]+]]
+; DBGINFO-NEXT:      #dbg_value(i8 poison, [[META90:![0-9]+]], !DIExpression(), [[DBG97]])
 ; DBGINFO-NEXT:    switch i3 [[V1:%.*]], label [[EXIT:%.*]] [
 ; DBGINFO-NEXT:      i3 0, label [[FOR_END:%.*]]
 ; DBGINFO-NEXT:      i3 -1, label [[FOR_END]]
@@ -215,11 +210,10 @@ define void @PR36225(i32 %a, i32 %b, i1 %c1, i3 %v1, i3 %v2) {
 ; DBGINFO-NEXT:      i3 -1, label [[FOR_END]]
 ; DBGINFO-NEXT:    ], !dbg [[DBG99:![0-9]+]]
 ; DBGINFO:       for.end:
-; DBGINFO-NEXT:    [[H:%.*]] = phi i8 [ [[SPEC_SELECT]], [[FOR_BODY3_US]] ], [ [[SPEC_SELECT]], [[FOR_BODY3_US]] ], [ 0, [[FOR_BODY3]] ], [ 0, [[FOR_BODY3]] ], !dbg [[DBG100:![0-9]+]]
-; DBGINFO-NEXT:      #dbg_value(i8 [[H]], [[META91:![0-9]+]], !DIExpression(), [[DBG100]])
-; DBGINFO-NEXT:    [[CONV:%.*]] = zext nneg i8 [[H]] to i32, !dbg [[DBG101:![0-9]+]]
-; DBGINFO-NEXT:      #dbg_value(i32 [[CONV]], [[META92:![0-9]+]], !DIExpression(), [[DBG101]])
-; DBGINFO-NEXT:    [[CMP:%.*]] = icmp sgt i32 [[A:%.*]], [[CONV]], !dbg [[DBG102:![0-9]+]]
+; DBGINFO-NEXT:    [[H:%.*]] = phi i32 [ [[SPEC_SELECT]], [[FOR_BODY3_US]] ], [ [[SPEC_SELECT]], [[FOR_BODY3_US]] ], [ 0, [[FOR_BODY3]] ], [ 0, [[FOR_BODY3]] ], !dbg [[DBG100:![0-9]+]]
+; DBGINFO-NEXT:      #dbg_value(i32 [[H]], [[META91:![0-9]+]], !DIExpression(), [[DBG100]])
+; DBGINFO-NEXT:      #dbg_value(i32 [[H]], [[META92:![0-9]+]], !DIExpression(), [[META101:![0-9]+]])
+; DBGINFO-NEXT:    [[CMP:%.*]] = icmp sgt i32 [[A:%.*]], [[H]], !dbg [[DBG102:![0-9]+]]
 ; DBGINFO-NEXT:      #dbg_value(i1 [[CMP]], [[META93:![0-9]+]], !DIExpression(), [[DBG102]])
 ; DBGINFO-NEXT:    br i1 [[CMP]], label [[EXIT]], label [[EXIT2:%.*]], !dbg [[DBG103:![0-9]+]]
 ; DBGINFO:       exit2:

>From 2a4ddf6967486ef6ec00a20540adf72457296178 Mon Sep 17 00:00:00 2001
From: John Brawn <john.brawn at arm.com>
Date: Thu, 6 Aug 2026 15:48:42 +0100
Subject: [PATCH 3/7] Add NoSignedWrap comment

---
 llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
index 2d134c8458882..f77c0ba15ebfe 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
@@ -1613,8 +1613,8 @@ Instruction *InstCombinerImpl::visitZExt(ZExtInst &Zext) {
   // meaning they remain in the range of a signed SrcTy so we won't need to
   // clear any high bits.
   bool EvaluateAsSigned =
-      Zext.hasNonNeg() &&
-      TypeEvaluationHelper::canEvaluateSExtd(Src, DestTy, true);
+      Zext.hasNonNeg() && TypeEvaluationHelper::canEvaluateSExtd(
+                              Src, DestTy, /*NoSignedWrap=*/true);
 
   // Try to extend the entire expression tree to the wide destination type.
   unsigned BitsToClear = 0;

>From 49424e869037d8267450de708197d39d05a2eb57 Mon Sep 17 00:00:00 2001
From: John Brawn <john.brawn at arm.com>
Date: Mon, 10 Aug 2026 17:45:42 +0100
Subject: [PATCH 4/7] Don't need to check ComputeNumSignBits

---
 .../Transforms/InstCombine/InstCombineCasts.cpp   | 15 +++++++--------
 llvm/test/Transforms/InstCombine/zext-nneg.ll     |  2 +-
 2 files changed, 8 insertions(+), 9 deletions(-)

diff --git a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
index f77c0ba15ebfe..52e1d54589963 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
@@ -1642,14 +1642,13 @@ Instruction *InstCombinerImpl::visitZExt(ZExtInst &Zext) {
     uint32_t DestBitSize = DestTy->getScalarSizeInBits();
 
     // If the high bits are already filled with zeros, just replace this
-    // cast with the result. If we've evaluated as a signed expressions then
-    // instead check that the high bits are the sign bit, which we know is zero.
-    if (EvaluateAsSigned
-            ? (ComputeNumSignBits(Res, &Zext) > DestBitSize - SrcBitsKept)
-            : MaskedValueIsZero(
-                  Res,
-                  APInt::getHighBitsSet(DestBitSize, DestBitSize - SrcBitsKept),
-                  &Zext))
+    // cast with the result. If we've evaluated as a signed expressions then we
+    // know the high bits are all zero, as canEvaluateSExtd doesn't permit
+    // anything that would set the high bits.
+    if (EvaluateAsSigned ||
+        MaskedValueIsZero(
+            Res, APInt::getHighBitsSet(DestBitSize, DestBitSize - SrcBitsKept),
+            &Zext))
       return replaceInstUsesWith(Zext, Res);
 
     // We need to emit an AND to clear the high bits.
diff --git a/llvm/test/Transforms/InstCombine/zext-nneg.ll b/llvm/test/Transforms/InstCombine/zext-nneg.ll
index bb75d37a7824f..deda3922467a8 100644
--- a/llvm/test/Transforms/InstCombine/zext-nneg.ll
+++ b/llvm/test/Transforms/InstCombine/zext-nneg.ll
@@ -136,7 +136,7 @@ define i64 @zext_nneg_of_zext_large_negative(i16 %arg) {
 ; CHECK-SAME: i16 [[ARG:%.*]]) {
 ; CHECK-NEXT:  [[ENTRY:.*:]]
 ; CHECK-NEXT:    [[SEXT:%.*]] = zext i16 [[ARG]] to i64
-; CHECK-NEXT:    [[ADD:%.*]] = add nuw nsw i64 [[SEXT]], 2147516415
+; CHECK-NEXT:    [[ADD:%.*]] = add nuw nsw i64 [[SEXT]], -2147450881
 ; CHECK-NEXT:    ret i64 [[ADD]]
 ;
 entry:

>From a7e0a2b7f1a80d951e0d1175c34ae4577a868f2c Mon Sep 17 00:00:00 2001
From: John Brawn <john.brawn at arm.com>
Date: Tue, 11 Aug 2026 11:51:49 +0100
Subject: [PATCH 5/7] Add zext_of_trunc test

---
 llvm/test/Transforms/InstCombine/zext-nneg.ll | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/llvm/test/Transforms/InstCombine/zext-nneg.ll b/llvm/test/Transforms/InstCombine/zext-nneg.ll
index deda3922467a8..4dc093d6a06dd 100644
--- a/llvm/test/Transforms/InstCombine/zext-nneg.ll
+++ b/llvm/test/Transforms/InstCombine/zext-nneg.ll
@@ -231,3 +231,18 @@ entry:
   %zext = zext nneg i32 %and to i64
   ret i64 %zext
 }
+
+; FIXME: The and isn't needed here, as the trunc nsw means we know the high bits
+; are all equal to the sign bit, and zext nneg means we know the sign bit is
+; zero.
+define i32 @zext_of_trunc(i64 %arg) {
+; CHECK-LABEL: define i32 @zext_of_trunc(
+; CHECK-SAME: i64 [[ARG:%.*]]) {
+; CHECK-NEXT:    [[TMP1:%.*]] = trunc i64 [[ARG]] to i32
+; CHECK-NEXT:    [[ZEXT:%.*]] = and i32 [[TMP1]], 65535
+; CHECK-NEXT:    ret i32 [[ZEXT]]
+;
+  %trunc = trunc nsw i64 %arg to i16
+  %zext = zext nneg i16 %trunc to i32
+  ret i32 %zext
+}

>From e6e9ddbe0561ae1352b5b73fd0a99795ca53005f Mon Sep 17 00:00:00 2001
From: John Brawn <john.brawn at arm.com>
Date: Tue, 11 Aug 2026 19:25:42 +0100
Subject: [PATCH 6/7] Revert "Don't need to check ComputeNumSignBits"

This reverts commit 49424e869037d8267450de708197d39d05a2eb57.
---
 .../Transforms/InstCombine/InstCombineCasts.cpp   | 15 ++++++++-------
 llvm/test/Transforms/InstCombine/zext-nneg.ll     |  2 +-
 2 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
index 52e1d54589963..f77c0ba15ebfe 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
@@ -1642,13 +1642,14 @@ Instruction *InstCombinerImpl::visitZExt(ZExtInst &Zext) {
     uint32_t DestBitSize = DestTy->getScalarSizeInBits();
 
     // If the high bits are already filled with zeros, just replace this
-    // cast with the result. If we've evaluated as a signed expressions then we
-    // know the high bits are all zero, as canEvaluateSExtd doesn't permit
-    // anything that would set the high bits.
-    if (EvaluateAsSigned ||
-        MaskedValueIsZero(
-            Res, APInt::getHighBitsSet(DestBitSize, DestBitSize - SrcBitsKept),
-            &Zext))
+    // cast with the result. If we've evaluated as a signed expressions then
+    // instead check that the high bits are the sign bit, which we know is zero.
+    if (EvaluateAsSigned
+            ? (ComputeNumSignBits(Res, &Zext) > DestBitSize - SrcBitsKept)
+            : MaskedValueIsZero(
+                  Res,
+                  APInt::getHighBitsSet(DestBitSize, DestBitSize - SrcBitsKept),
+                  &Zext))
       return replaceInstUsesWith(Zext, Res);
 
     // We need to emit an AND to clear the high bits.
diff --git a/llvm/test/Transforms/InstCombine/zext-nneg.ll b/llvm/test/Transforms/InstCombine/zext-nneg.ll
index 4dc093d6a06dd..fd8490fdb3b32 100644
--- a/llvm/test/Transforms/InstCombine/zext-nneg.ll
+++ b/llvm/test/Transforms/InstCombine/zext-nneg.ll
@@ -136,7 +136,7 @@ define i64 @zext_nneg_of_zext_large_negative(i16 %arg) {
 ; CHECK-SAME: i16 [[ARG:%.*]]) {
 ; CHECK-NEXT:  [[ENTRY:.*:]]
 ; CHECK-NEXT:    [[SEXT:%.*]] = zext i16 [[ARG]] to i64
-; CHECK-NEXT:    [[ADD:%.*]] = add nuw nsw i64 [[SEXT]], -2147450881
+; CHECK-NEXT:    [[ADD:%.*]] = add nuw nsw i64 [[SEXT]], 2147516415
 ; CHECK-NEXT:    ret i64 [[ADD]]
 ;
 entry:

>From fee28c1c4d7b03f046436e78c6f4d77576c1836b Mon Sep 17 00:00:00 2001
From: John Brawn <john.brawn at arm.com>
Date: Wed, 12 Aug 2026 14:45:59 +0100
Subject: [PATCH 7/7] Add zext_of_trunc_same_size test

This is an example where not checking ComputeNumSignBits gives the wrong result
---
 llvm/test/Transforms/InstCombine/zext-nneg.ll | 19 +++++++++++++++++--
 1 file changed, 17 insertions(+), 2 deletions(-)

diff --git a/llvm/test/Transforms/InstCombine/zext-nneg.ll b/llvm/test/Transforms/InstCombine/zext-nneg.ll
index fd8490fdb3b32..20188f605652e 100644
--- a/llvm/test/Transforms/InstCombine/zext-nneg.ll
+++ b/llvm/test/Transforms/InstCombine/zext-nneg.ll
@@ -232,11 +232,26 @@ entry:
   ret i64 %zext
 }
 
+; trunc then zext nneg to the same size as before isn't a no-op, and we need to
+; have an and.
+define i64 @zext_of_trunc_same_size(i64 %arg) {
+; CHECK-LABEL: define i64 @zext_of_trunc_same_size(
+; CHECK-SAME: i64 [[ARG:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[ZEXT:%.*]] = and i64 [[ARG]], 4294967295
+; CHECK-NEXT:    ret i64 [[ZEXT]]
+;
+entry:
+  %trunc = trunc i64 %arg to i32
+  %zext = zext nneg i32 %trunc to i64
+  ret i64 %zext
+}
+
 ; FIXME: The and isn't needed here, as the trunc nsw means we know the high bits
 ; are all equal to the sign bit, and zext nneg means we know the sign bit is
 ; zero.
-define i32 @zext_of_trunc(i64 %arg) {
-; CHECK-LABEL: define i32 @zext_of_trunc(
+define i32 @zext_of_trunc_nsw(i64 %arg) {
+; CHECK-LABEL: define i32 @zext_of_trunc_nsw(
 ; CHECK-SAME: i64 [[ARG:%.*]]) {
 ; CHECK-NEXT:    [[TMP1:%.*]] = trunc i64 [[ARG]] to i32
 ; CHECK-NEXT:    [[ZEXT:%.*]] = and i32 [[TMP1]], 65535



More information about the llvm-commits mailing list