[llvm] [DAGCombiner] Preserve nsw when folding (mul x, 2^c) to (shl x, c) (PR #192366)

Jim Lin via llvm-commits llvm-commits at lists.llvm.org
Sun Apr 19 22:49:33 PDT 2026


https://github.com/tclin914 updated https://github.com/llvm/llvm-project/pull/192366

>From 583fa976cf56f85abc215c524b14dd602bc0218f Mon Sep 17 00:00:00 2001
From: Jim Lin <jim at andestech.com>
Date: Mon, 20 Apr 2026 02:11:29 +0000
Subject: [PATCH 1/2] [RISCV] Pre-commit test for (mul x, 2^c) -> (shl x, c)
 flag preservation

Baseline MIR test showing that DAGCombiner folds mul-by-power-of-two
into SLLI. Currently nsw is not preserved on the shift. Preparatory for
a follow-up that preserves nsw when the shift amount is less than
BitWidth - 1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply at anthropic.com>
---
 llvm/test/CodeGen/RISCV/mul-to-shl-nsw.ll | 58 +++++++++++++++++++++++
 1 file changed, 58 insertions(+)
 create mode 100644 llvm/test/CodeGen/RISCV/mul-to-shl-nsw.ll

diff --git a/llvm/test/CodeGen/RISCV/mul-to-shl-nsw.ll b/llvm/test/CodeGen/RISCV/mul-to-shl-nsw.ll
new file mode 100644
index 0000000000000..7dd94654a79f4
--- /dev/null
+++ b/llvm/test/CodeGen/RISCV/mul-to-shl-nsw.ll
@@ -0,0 +1,58 @@
+; NOTE: Assertions have been autogenerated by utils/update_mir_test_checks.py
+; RUN: llc -mtriple=riscv64 -stop-after=finalize-isel -o - %s | FileCheck %s
+
+; Verifies that (mul x, 2^c) -> (shl x, c) fold fires
+; for nsw, nuw, and plain mul by power-of-two, including the signed-minimum
+; boundary case.
+
+define i64 @mul_nsw_pow2(i64 %x) {
+  ; CHECK-LABEL: name: mul_nsw_pow2
+  ; CHECK: bb.0 (%ir-block.0):
+  ; CHECK-NEXT:   liveins: $x10
+  ; CHECK-NEXT: {{  $}}
+  ; CHECK-NEXT:   [[COPY:%[0-9]+]]:gpr = COPY $x10
+  ; CHECK-NEXT:   [[SLLI:%[0-9]+]]:gpr = SLLI [[COPY]], 2
+  ; CHECK-NEXT:   $x10 = COPY [[SLLI]]
+  ; CHECK-NEXT:   PseudoRET implicit $x10
+  %mul = mul nsw i64 %x, 4
+  ret i64 %mul
+}
+
+define i64 @mul_nsw_pow2_signmin(i64 %x) {
+  ; CHECK-LABEL: name: mul_nsw_pow2_signmin
+  ; CHECK: bb.0 (%ir-block.0):
+  ; CHECK-NEXT:   liveins: $x10
+  ; CHECK-NEXT: {{  $}}
+  ; CHECK-NEXT:   [[COPY:%[0-9]+]]:gpr = COPY $x10
+  ; CHECK-NEXT:   [[SLLI:%[0-9]+]]:gpr = SLLI [[COPY]], 63
+  ; CHECK-NEXT:   $x10 = COPY [[SLLI]]
+  ; CHECK-NEXT:   PseudoRET implicit $x10
+  %mul = mul nsw i64 %x, -9223372036854775808
+  ret i64 %mul
+}
+
+define i64 @mul_no_flags_pow2(i64 %x) {
+  ; CHECK-LABEL: name: mul_no_flags_pow2
+  ; CHECK: bb.0 (%ir-block.0):
+  ; CHECK-NEXT:   liveins: $x10
+  ; CHECK-NEXT: {{  $}}
+  ; CHECK-NEXT:   [[COPY:%[0-9]+]]:gpr = COPY $x10
+  ; CHECK-NEXT:   [[SLLI:%[0-9]+]]:gpr = SLLI [[COPY]], 2
+  ; CHECK-NEXT:   $x10 = COPY [[SLLI]]
+  ; CHECK-NEXT:   PseudoRET implicit $x10
+  %mul = mul i64 %x, 4
+  ret i64 %mul
+}
+
+define i64 @mul_nuw_pow2(i64 %x) {
+  ; CHECK-LABEL: name: mul_nuw_pow2
+  ; CHECK: bb.0 (%ir-block.0):
+  ; CHECK-NEXT:   liveins: $x10
+  ; CHECK-NEXT: {{  $}}
+  ; CHECK-NEXT:   [[COPY:%[0-9]+]]:gpr = COPY $x10
+  ; CHECK-NEXT:   [[SLLI:%[0-9]+]]:gpr = nuw SLLI [[COPY]], 2
+  ; CHECK-NEXT:   $x10 = COPY [[SLLI]]
+  ; CHECK-NEXT:   PseudoRET implicit $x10
+  %mul = mul nuw i64 %x, 4
+  ret i64 %mul
+}

>From a561e354ec6abbe1aec424496a69af74929639dd Mon Sep 17 00:00:00 2001
From: Jim Lin <jim at andestech.com>
Date: Mon, 20 Apr 2026 02:11:37 +0000
Subject: [PATCH 2/2] [DAGCombiner] Preserve nsw when folding (mul x, 2^c) to
 (shl x, c)

When the multiplier is a power of two strictly less than 2^(BitWidth-1),
the signed multiplication cannot overflow any more than the equivalent
shift, so nsw transfers from the mul to the shl. At 2^(BitWidth-1) the
multiplier is the signed minimum and the equivalence breaks, so keep
the existing behaviour of dropping nsw in that case.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply at anthropic.com>
---
 llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp | 6 +++++-
 llvm/test/CodeGen/RISCV/mul-to-shl-nsw.ll     | 2 +-
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
index aee5aabdc9e02..0625aa3f324f8 100644
--- a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
+++ b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
@@ -4832,7 +4832,11 @@ template <class MatchContextClass> SDValue DAGCombiner::visitMUL(SDNode *N) {
       SDValue Trunc = DAG.getZExtOrTrunc(LogBase2, DL, ShiftVT);
       SDNodeFlags Flags;
       Flags.setNoUnsignedWrap(N->getFlags().hasNoUnsignedWrap());
-      // TODO: Preserve setNoSignedWrap if LogBase2 isn't BitWidth - 1.
+      // Preserve nsw when the shift amount is strictly less than BitWidth - 1,
+      // i.e. the multiplier is not the signed minimum value.
+      if (N->getFlags().hasNoSignedWrap() && N1IsConst &&
+          ConstValue1.logBase2() < BitWidth - 1)
+        Flags.setNoSignedWrap(true);
       return Matcher.getNode(ISD::SHL, DL, VT, N0, Trunc, Flags);
     }
   }
diff --git a/llvm/test/CodeGen/RISCV/mul-to-shl-nsw.ll b/llvm/test/CodeGen/RISCV/mul-to-shl-nsw.ll
index 7dd94654a79f4..a81a3698a41e1 100644
--- a/llvm/test/CodeGen/RISCV/mul-to-shl-nsw.ll
+++ b/llvm/test/CodeGen/RISCV/mul-to-shl-nsw.ll
@@ -11,7 +11,7 @@ define i64 @mul_nsw_pow2(i64 %x) {
   ; CHECK-NEXT:   liveins: $x10
   ; CHECK-NEXT: {{  $}}
   ; CHECK-NEXT:   [[COPY:%[0-9]+]]:gpr = COPY $x10
-  ; CHECK-NEXT:   [[SLLI:%[0-9]+]]:gpr = SLLI [[COPY]], 2
+  ; CHECK-NEXT:   [[SLLI:%[0-9]+]]:gpr = nsw SLLI [[COPY]], 2
   ; CHECK-NEXT:   $x10 = COPY [[SLLI]]
   ; CHECK-NEXT:   PseudoRET implicit $x10
   %mul = mul nsw i64 %x, 4



More information about the llvm-commits mailing list