[llvm] [DAGCombiner] Fold NaN-guard fptosi/fptoui select to saturating variant (PR #201435)

Adel Ejjeh via llvm-commits llvm-commits at lists.llvm.org
Fri Jul 17 08:49:36 PDT 2026


https://github.com/adelejjeh updated https://github.com/llvm/llvm-project/pull/201435

>From b45036b003493a19926b7135f65509393c822372 Mon Sep 17 00:00:00 2001
From: Adel Ejjeh <adel.ejjeh at amd.com>
Date: Wed, 3 Jun 2026 09:39:39 -0500
Subject: [PATCH 1/3] [DAGCombiner] Fold NaN-guard fptosi/fptoui select to
 saturating variant
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Fold select (setcc X, 0, SETUO), 0, (fp_to_sint/fp_to_uint X) to
fp_to_sint_sat/fp_to_uint_sat when the target supports it. Also looks
through an AND mask on the conversion result.

For in-range values the result is identical. For NaN both return 0.
For out-of-range, fp_to_sint produces poison while fp_to_sint_sat
produces a saturated value — replacing poison with a defined value is
valid refinement.

Alive2: https://alive2.llvm.org/ce/z/ZbFdM4

Assisted-by: Claude Code
---
 llvm/include/llvm/CodeGen/TargetLowering.h    |   5 +-
 llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp |  61 ++++++++
 .../CodeGen/AArch64/fptoi-nan-guard-fold.ll   | 127 ++++++++++++++++
 .../CodeGen/AMDGPU/fptoi-nan-guard-fold.ll    | 140 ++++++++++++++++++
 4 files changed, 331 insertions(+), 2 deletions(-)
 create mode 100644 llvm/test/CodeGen/AArch64/fptoi-nan-guard-fold.ll
 create mode 100644 llvm/test/CodeGen/AMDGPU/fptoi-nan-guard-fold.ll

diff --git a/llvm/include/llvm/CodeGen/TargetLowering.h b/llvm/include/llvm/CodeGen/TargetLowering.h
index 82c47cce0f522..d7c8690821f4c 100644
--- a/llvm/include/llvm/CodeGen/TargetLowering.h
+++ b/llvm/include/llvm/CodeGen/TargetLowering.h
@@ -3596,8 +3596,9 @@ class LLVM_ABI TargetLoweringBase {
   /// passed to the fp16 to fp conversion library function.
   virtual bool shouldKeepZExtForFP16Conv() const { return false; }
 
-  /// Should we generate fp_to_si_sat and fp_to_ui_sat from type FPVT to type VT
-  /// from min(max(fptoi)) saturation patterns.
+  /// Should we generate fp_to_si_sat and fp_to_ui_sat from type FPVT to type
+  /// VT. Used when folding idioms into a saturating fp-to-int conversion, such
+  /// as min(max(fptoi)) clamps or NaN-guarded selects.
   virtual bool shouldConvertFpToSat(unsigned Op, EVT FPVT, EVT VT) const {
     return isOperationLegalOrCustom(Op, VT);
   }
diff --git a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
index 0c9820fb64de9..ef080ac97a642 100644
--- a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
+++ b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
@@ -6283,6 +6283,62 @@ static SDValue PerformUMinFpToSatCombine(SDValue N0, SDValue N1, SDValue N2,
   return DAG.getZExtOrTrunc(Sat, SDLoc(N0), N3.getValueType());
 }
 
+// Fold a NaN-guard select of fp_to_sint/fp_to_uint into the saturating
+// variant, which returns 0 for NaN. Matches both SETUO/SETO forms and
+// optionally looks through an AND mask on the conversion result.
+static SDValue PerformNanGuardFpToSatCombine(SDNode *N, SelectionDAG &DAG) {
+  EVT VT = N->getValueType(0);
+  SDLoc DL(N);
+
+  // Match an isnan-guarded select. Require the compare to be single-use since
+  // folding does not remove it otherwise.
+  //   select (setcc X, RHS, uno), 0, guarded
+  //   select (setcc X, RHS, ord), guarded, 0
+  SDValue X, CmpRHS, GuardedVal;
+  auto NaNCheck = [&](ISD::CondCode CC) {
+    return m_OneUse(
+        m_SetCC(m_Value(X), m_Value(CmpRHS), m_SpecificCondCode(CC)));
+  };
+  if (!sd_match(N, m_SelectLike(NaNCheck(ISD::SETUO), m_Zero(),
+                                m_Value(GuardedVal))) &&
+      !sd_match(
+          N, m_SelectLike(NaNCheck(ISD::SETO), m_Value(GuardedVal), m_Zero())))
+    return SDValue();
+
+  // The compare must test X for NaN: RHS is +/-0.0 (canonical isnan) or X
+  // itself (self-compare form).
+  auto *CmpRHSC = isConstOrConstSplatFP(CmpRHS);
+  if (CmpRHS != X && !(CmpRHSC && CmpRHSC->isZero()))
+    return SDValue();
+
+  // Peel an optional AND mask, then require fp_to_sint/fp_to_uint of the same
+  // X.
+  SDValue Mask;
+  SDValue Conv = GuardedVal;
+  if (Conv.getOpcode() == ISD::AND) {
+    Mask = Conv.getOperand(1);
+    Conv = Conv.getOperand(0);
+  }
+
+  unsigned NewOpc;
+  if (sd_match(Conv, m_FPToSI(m_Specific(X))))
+    NewOpc = ISD::FP_TO_SINT_SAT;
+  else if (sd_match(Conv, m_FPToUI(m_Specific(X))))
+    NewOpc = ISD::FP_TO_UINT_SAT;
+  else
+    return SDValue();
+
+  if (!DAG.getTargetLoweringInfo().shouldConvertFpToSat(NewOpc,
+                                                        X.getValueType(), VT))
+    return SDValue();
+
+  SDValue Sat =
+      DAG.getNode(NewOpc, DL, VT, X, DAG.getValueType(VT.getScalarType()));
+  if (Mask)
+    Sat = DAG.getNode(ISD::AND, DL, VT, Sat, Mask);
+  return Sat;
+}
+
 SDValue DAGCombiner::visitIMINMAX(SDNode *N) {
   SDValue N0 = N->getOperand(0);
   SDValue N1 = N->getOperand(1);
@@ -13178,6 +13234,9 @@ SDValue DAGCombiner::visitSELECT(SDNode *N) {
       }
     }
 
+    if (SDValue S = PerformNanGuardFpToSatCombine(N, DAG))
+      return S;
+
     if (TLI.isOperationLegal(ISD::SELECT_CC, VT) ||
         (!LegalOperations &&
          TLI.isOperationLegalOrCustom(ISD::SELECT_CC, VT))) {
@@ -14187,6 +14246,8 @@ SDValue DAGCombiner::visitVSELECT(SDNode *N) {
       return S;
     if (SDValue S = PerformUMinFpToSatCombine(LHS, RHS, N1, N2, CC, DAG))
       return S;
+    if (SDValue S = PerformNanGuardFpToSatCombine(N, DAG))
+      return S;
 
     // If this select has a condition (setcc) with narrower operands than the
     // select, try to widen the compare to match the select width.
diff --git a/llvm/test/CodeGen/AArch64/fptoi-nan-guard-fold.ll b/llvm/test/CodeGen/AArch64/fptoi-nan-guard-fold.ll
new file mode 100644
index 0000000000000..1fba2b2f9647c
--- /dev/null
+++ b/llvm/test/CodeGen/AArch64/fptoi-nan-guard-fold.ll
@@ -0,0 +1,127 @@
+; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py
+; RUN: llc -mtriple=aarch64 < %s | FileCheck %s
+
+; The generic DAG combiner folds select(setcc X, 0, SETUO), 0, (fp_to_sint X)
+; into fp_to_sint_sat when the target supports it. On AArch64, fcvtzs/fcvtzu
+; natively saturate for NaN, so fp_to_sint_sat lowers to a single instruction.
+
+;; Positive tests
+
+define i32 @nan_guard_fptosi_f32(float %x) {
+; CHECK-LABEL: nan_guard_fptosi_f32:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    fcvtzs w0, s0
+; CHECK-NEXT:    ret
+  %cmp = fcmp uno float %x, 0.000000e+00
+  %conv = fptosi float %x to i32
+  %sel = select i1 %cmp, i32 0, i32 %conv
+  ret i32 %sel
+}
+
+define i32 @nan_guard_fptosi_ord_f32(float %x) {
+; CHECK-LABEL: nan_guard_fptosi_ord_f32:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    fcvtzs w0, s0
+; CHECK-NEXT:    ret
+  %cmp = fcmp ord float %x, 0.000000e+00
+  %conv = fptosi float %x to i32
+  %sel = select i1 %cmp, i32 %conv, i32 0
+  ret i32 %sel
+}
+
+define i32 @nan_guard_fptoui_f32(float %x) {
+; CHECK-LABEL: nan_guard_fptoui_f32:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    fcvtzu w0, s0
+; CHECK-NEXT:    ret
+  %cmp = fcmp uno float %x, 0.000000e+00
+  %conv = fptoui float %x to i32
+  %sel = select i1 %cmp, i32 0, i32 %conv
+  ret i32 %sel
+}
+
+define i32 @nan_guard_fptoui_ord_f32(float %x) {
+; CHECK-LABEL: nan_guard_fptoui_ord_f32:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    fcvtzu w0, s0
+; CHECK-NEXT:    ret
+  %cmp = fcmp ord float %x, 0.000000e+00
+  %conv = fptoui float %x to i32
+  %sel = select i1 %cmp, i32 %conv, i32 0
+  ret i32 %sel
+}
+
+define i32 @nan_guard_fptosi_and_mask(float %x) {
+; CHECK-LABEL: nan_guard_fptosi_and_mask:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    fcvtzs w8, s0
+; CHECK-NEXT:    and w0, w8, #0x3
+; CHECK-NEXT:    ret
+  %cmp = fcmp uno float %x, 0.000000e+00
+  %conv = fptosi float %x to i32
+  %and = and i32 %conv, 3
+  %sel = select i1 %cmp, i32 0, i32 %and
+  ret i32 %sel
+}
+
+define i32 @nan_guard_fptosi_f64(double %x) {
+; CHECK-LABEL: nan_guard_fptosi_f64:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    fcvtzs w0, d0
+; CHECK-NEXT:    ret
+  %cmp = fcmp uno double %x, 0.000000e+00
+  %conv = fptosi double %x to i32
+  %sel = select i1 %cmp, i32 0, i32 %conv
+  ret i32 %sel
+}
+
+define i32 @nan_guard_fptosi_cmp_self(float %x) {
+; CHECK-LABEL: nan_guard_fptosi_cmp_self:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    fcvtzs w0, s0
+; CHECK-NEXT:    ret
+  %cmp = fcmp uno float %x, %x
+  %conv = fptosi float %x to i32
+  %sel = select i1 %cmp, i32 0, i32 %conv
+  ret i32 %sel
+}
+
+define <4 x i32> @nan_guard_fptosi_v4f32(<4 x float> %x) {
+; CHECK-LABEL: nan_guard_fptosi_v4f32:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    fcvtzs v0.4s, v0.4s
+; CHECK-NEXT:    ret
+  %cmp = fcmp uno <4 x float> %x, zeroinitializer
+  %conv = fptosi <4 x float> %x to <4 x i32>
+  %sel = select <4 x i1> %cmp, <4 x i32> zeroinitializer, <4 x i32> %conv
+  ret <4 x i32> %sel
+}
+
+;; Negative tests
+
+define i32 @nan_guard_nonzero_constant(float %x) {
+; CHECK-LABEL: nan_guard_nonzero_constant:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    fcvtzs w8, s0
+; CHECK-NEXT:    fcmp s0, s0
+; CHECK-NEXT:    mov w9, #42 // =0x2a
+; CHECK-NEXT:    csel w0, w9, w8, vs
+; CHECK-NEXT:    ret
+  %cmp = fcmp uno float %x, 0.000000e+00
+  %conv = fptosi float %x to i32
+  %sel = select i1 %cmp, i32 42, i32 %conv
+  ret i32 %sel
+}
+
+define i32 @nan_guard_mismatched_operands(float %x, float %y) {
+; CHECK-LABEL: nan_guard_mismatched_operands:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    fcvtzs w8, s1
+; CHECK-NEXT:    fcmp s0, s0
+; CHECK-NEXT:    csel w0, wzr, w8, vs
+; CHECK-NEXT:    ret
+  %cmp = fcmp uno float %x, 0.000000e+00
+  %conv = fptosi float %y to i32
+  %sel = select i1 %cmp, i32 0, i32 %conv
+  ret i32 %sel
+}
diff --git a/llvm/test/CodeGen/AMDGPU/fptoi-nan-guard-fold.ll b/llvm/test/CodeGen/AMDGPU/fptoi-nan-guard-fold.ll
new file mode 100644
index 0000000000000..a4334a4fb8ffc
--- /dev/null
+++ b/llvm/test/CodeGen/AMDGPU/fptoi-nan-guard-fold.ll
@@ -0,0 +1,140 @@
+; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py
+; RUN: llc -mtriple=amdgcn-amd-amdhsa -mcpu=gfx90a < %s | FileCheck %s
+
+; The generic DAG combiner folds select(setcc X, 0, SETUO), 0, (fp_to_sint X)
+; into fp_to_sint_sat when the target supports it. On AMDGPU, the V_CVT
+; instructions already return 0 for NaN, so fp_to_sint_sat lowers to a single
+; conversion instruction with no extra compare/select overhead.
+
+;; Positive tests
+
+define i32 @nan_guard_fptosi_f32(float %x) {
+; CHECK-LABEL: nan_guard_fptosi_f32:
+; CHECK:       ; %bb.0:
+; CHECK-NEXT:    s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+; CHECK-NEXT:    v_cvt_i32_f32_e32 v0, v0
+; CHECK-NEXT:    s_setpc_b64 s[30:31]
+  %cmp = fcmp uno float %x, 0.000000e+00
+  %conv = fptosi float %x to i32
+  %sel = select i1 %cmp, i32 0, i32 %conv
+  ret i32 %sel
+}
+
+define i32 @nan_guard_fptosi_ord_f32(float %x) {
+; CHECK-LABEL: nan_guard_fptosi_ord_f32:
+; CHECK:       ; %bb.0:
+; CHECK-NEXT:    s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+; CHECK-NEXT:    v_cvt_i32_f32_e32 v0, v0
+; CHECK-NEXT:    s_setpc_b64 s[30:31]
+  %cmp = fcmp ord float %x, 0.000000e+00
+  %conv = fptosi float %x to i32
+  %sel = select i1 %cmp, i32 %conv, i32 0
+  ret i32 %sel
+}
+
+define i32 @nan_guard_fptoui_f32(float %x) {
+; CHECK-LABEL: nan_guard_fptoui_f32:
+; CHECK:       ; %bb.0:
+; CHECK-NEXT:    s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+; CHECK-NEXT:    v_cvt_u32_f32_e32 v0, v0
+; CHECK-NEXT:    s_setpc_b64 s[30:31]
+  %cmp = fcmp uno float %x, 0.000000e+00
+  %conv = fptoui float %x to i32
+  %sel = select i1 %cmp, i32 0, i32 %conv
+  ret i32 %sel
+}
+
+define i32 @nan_guard_fptoui_ord_f32(float %x) {
+; CHECK-LABEL: nan_guard_fptoui_ord_f32:
+; CHECK:       ; %bb.0:
+; CHECK-NEXT:    s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+; CHECK-NEXT:    v_cvt_u32_f32_e32 v0, v0
+; CHECK-NEXT:    s_setpc_b64 s[30:31]
+  %cmp = fcmp ord float %x, 0.000000e+00
+  %conv = fptoui float %x to i32
+  %sel = select i1 %cmp, i32 %conv, i32 0
+  ret i32 %sel
+}
+
+define i32 @nan_guard_fptosi_and_mask(float %x) {
+; CHECK-LABEL: nan_guard_fptosi_and_mask:
+; CHECK:       ; %bb.0:
+; CHECK-NEXT:    s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+; CHECK-NEXT:    v_cvt_i32_f32_e32 v0, v0
+; CHECK-NEXT:    v_and_b32_e32 v0, 3, v0
+; CHECK-NEXT:    s_setpc_b64 s[30:31]
+  %cmp = fcmp uno float %x, 0.000000e+00
+  %conv = fptosi float %x to i32
+  %and = and i32 %conv, 3
+  %sel = select i1 %cmp, i32 0, i32 %and
+  ret i32 %sel
+}
+
+define i32 @nan_guard_fptosi_f64(double %x) {
+; CHECK-LABEL: nan_guard_fptosi_f64:
+; CHECK:       ; %bb.0:
+; CHECK-NEXT:    s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+; CHECK-NEXT:    v_cvt_i32_f64_e32 v0, v[0:1]
+; CHECK-NEXT:    s_setpc_b64 s[30:31]
+  %cmp = fcmp uno double %x, 0.000000e+00
+  %conv = fptosi double %x to i32
+  %sel = select i1 %cmp, i32 0, i32 %conv
+  ret i32 %sel
+}
+
+define i32 @nan_guard_fptosi_cmp_self(float %x) {
+; CHECK-LABEL: nan_guard_fptosi_cmp_self:
+; CHECK:       ; %bb.0:
+; CHECK-NEXT:    s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+; CHECK-NEXT:    v_cvt_i32_f32_e32 v0, v0
+; CHECK-NEXT:    s_setpc_b64 s[30:31]
+  %cmp = fcmp uno float %x, %x
+  %conv = fptosi float %x to i32
+  %sel = select i1 %cmp, i32 0, i32 %conv
+  ret i32 %sel
+}
+
+define <4 x i32> @nan_guard_fptosi_v4f32(<4 x float> %x) {
+; CHECK-LABEL: nan_guard_fptosi_v4f32:
+; CHECK:       ; %bb.0:
+; CHECK-NEXT:    s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+; CHECK-NEXT:    v_cvt_i32_f32_e32 v0, v0
+; CHECK-NEXT:    v_cvt_i32_f32_e32 v1, v1
+; CHECK-NEXT:    v_cvt_i32_f32_e32 v2, v2
+; CHECK-NEXT:    v_cvt_i32_f32_e32 v3, v3
+; CHECK-NEXT:    s_setpc_b64 s[30:31]
+  %cmp = fcmp uno <4 x float> %x, zeroinitializer
+  %conv = fptosi <4 x float> %x to <4 x i32>
+  %sel = select <4 x i1> %cmp, <4 x i32> zeroinitializer, <4 x i32> %conv
+  ret <4 x i32> %sel
+}
+
+;; Negative tests
+
+define i32 @nan_guard_nonzero_constant(float %x) {
+; CHECK-LABEL: nan_guard_nonzero_constant:
+; CHECK:       ; %bb.0:
+; CHECK-NEXT:    s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+; CHECK-NEXT:    v_cvt_i32_f32_e32 v1, v0
+; CHECK-NEXT:    v_cmp_o_f32_e32 vcc, v0, v0
+; CHECK-NEXT:    v_cndmask_b32_e32 v0, 42, v1, vcc
+; CHECK-NEXT:    s_setpc_b64 s[30:31]
+  %cmp = fcmp uno float %x, 0.000000e+00
+  %conv = fptosi float %x to i32
+  %sel = select i1 %cmp, i32 42, i32 %conv
+  ret i32 %sel
+}
+
+define i32 @nan_guard_mismatched_operands(float %x, float %y) {
+; CHECK-LABEL: nan_guard_mismatched_operands:
+; CHECK:       ; %bb.0:
+; CHECK-NEXT:    s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+; CHECK-NEXT:    v_cvt_i32_f32_e32 v1, v1
+; CHECK-NEXT:    v_cmp_o_f32_e32 vcc, v0, v0
+; CHECK-NEXT:    v_cndmask_b32_e32 v0, 0, v1, vcc
+; CHECK-NEXT:    s_setpc_b64 s[30:31]
+  %cmp = fcmp uno float %x, 0.000000e+00
+  %conv = fptosi float %y to i32
+  %sel = select i1 %cmp, i32 0, i32 %conv
+  ret i32 %sel
+}

>From e10192ddd212c9b26517999f40323351a50d3f5c Mon Sep 17 00:00:00 2001
From: Adel Ejjeh <adel.ejjeh at amd.com>
Date: Mon, 13 Jul 2026 09:21:02 -0500
Subject: [PATCH 2/3] [DAGCombiner] Simplify NaN-guard fptosi fold per review

Rewrite performNanGuardFpToSatCombine using sd_match with a new
m_AnyZeroFP matcher, require the compare to be single-use, and match
only the canonical +0.0 compare (dropping the self-compare form, which
earlier passes canonicalize away). Rename to lowercase per LLVM
convention and trim the comment.

Assisted-by: Claude Code
---
 llvm/include/llvm/CodeGen/SDPatternMatch.h    | 11 +++++
 llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp | 40 ++++++++-----------
 .../CodeGen/AArch64/fptoi-nan-guard-fold.ll   | 11 -----
 .../CodeGen/AMDGPU/fptoi-nan-guard-fold.ll    | 28 ++++++-------
 4 files changed, 39 insertions(+), 51 deletions(-)

diff --git a/llvm/include/llvm/CodeGen/SDPatternMatch.h b/llvm/include/llvm/CodeGen/SDPatternMatch.h
index b98b8837ad4f5..e2e20b14bb7f7 100644
--- a/llvm/include/llvm/CodeGen/SDPatternMatch.h
+++ b/llvm/include/llvm/CodeGen/SDPatternMatch.h
@@ -1367,6 +1367,17 @@ inline SpecificFP_match m_SpecificFP(double V) {
   return SpecificFP_match(APFloat(V));
 }
 
+struct AnyZeroFP_match {
+  template <typename MatchContext> bool match(const MatchContext &, SDValue N) {
+    if (ConstantFPSDNode *C = isConstOrConstSplatFP(N))
+      return C->isZero();
+    return false;
+  }
+};
+
+/// Match a floating-point +0.0 or -0.0 constant or splat.
+inline AnyZeroFP_match m_AnyZeroFP() { return AnyZeroFP_match(); }
+
 struct Negative_match {
   template <typename MatchContext>
   bool match(const MatchContext &Ctx, SDValue N) {
diff --git a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
index ef080ac97a642..280ef9981e84b 100644
--- a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
+++ b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
@@ -6284,31 +6284,23 @@ static SDValue PerformUMinFpToSatCombine(SDValue N0, SDValue N1, SDValue N2,
 }
 
 // Fold a NaN-guard select of fp_to_sint/fp_to_uint into the saturating
-// variant, which returns 0 for NaN. Matches both SETUO/SETO forms and
-// optionally looks through an AND mask on the conversion result.
-static SDValue PerformNanGuardFpToSatCombine(SDNode *N, SelectionDAG &DAG) {
+// variant, which returns 0 for NaN.
+static SDValue performNanGuardFpToSatCombine(SDNode *N, SelectionDAG &DAG) {
   EVT VT = N->getValueType(0);
   SDLoc DL(N);
 
-  // Match an isnan-guarded select. Require the compare to be single-use since
-  // folding does not remove it otherwise.
-  //   select (setcc X, RHS, uno), 0, guarded
-  //   select (setcc X, RHS, ord), guarded, 0
-  SDValue X, CmpRHS, GuardedVal;
-  auto NaNCheck = [&](ISD::CondCode CC) {
-    return m_OneUse(
-        m_SetCC(m_Value(X), m_Value(CmpRHS), m_SpecificCondCode(CC)));
-  };
-  if (!sd_match(N, m_SelectLike(NaNCheck(ISD::SETUO), m_Zero(),
-                                m_Value(GuardedVal))) &&
-      !sd_match(
-          N, m_SelectLike(NaNCheck(ISD::SETO), m_Value(GuardedVal), m_Zero())))
-    return SDValue();
-
-  // The compare must test X for NaN: RHS is +/-0.0 (canonical isnan) or X
-  // itself (self-compare form).
-  auto *CmpRHSC = isConstOrConstSplatFP(CmpRHS);
-  if (CmpRHS != X && !(CmpRHSC && CmpRHSC->isZero()))
+  // Match an isnan-guarded select, requiring the compare to be single-use:
+  //   select (setcc X, 0.0, uno), 0, guarded
+  //   select (setcc X, 0.0, ord), guarded, 0
+  SDValue X, GuardedVal;
+  if (!sd_match(N,
+                m_SelectLike(m_OneUse(m_SetCC(m_Value(X), m_AnyZeroFP(),
+                                              m_SpecificCondCode(ISD::SETUO))),
+                             m_Zero(), m_Value(GuardedVal))) &&
+      !sd_match(N,
+                m_SelectLike(m_OneUse(m_SetCC(m_Value(X), m_AnyZeroFP(),
+                                              m_SpecificCondCode(ISD::SETO))),
+                             m_Value(GuardedVal), m_Zero())))
     return SDValue();
 
   // Peel an optional AND mask, then require fp_to_sint/fp_to_uint of the same
@@ -13234,7 +13226,7 @@ SDValue DAGCombiner::visitSELECT(SDNode *N) {
       }
     }
 
-    if (SDValue S = PerformNanGuardFpToSatCombine(N, DAG))
+    if (SDValue S = performNanGuardFpToSatCombine(N, DAG))
       return S;
 
     if (TLI.isOperationLegal(ISD::SELECT_CC, VT) ||
@@ -14246,7 +14238,7 @@ SDValue DAGCombiner::visitVSELECT(SDNode *N) {
       return S;
     if (SDValue S = PerformUMinFpToSatCombine(LHS, RHS, N1, N2, CC, DAG))
       return S;
-    if (SDValue S = PerformNanGuardFpToSatCombine(N, DAG))
+    if (SDValue S = performNanGuardFpToSatCombine(N, DAG))
       return S;
 
     // If this select has a condition (setcc) with narrower operands than the
diff --git a/llvm/test/CodeGen/AArch64/fptoi-nan-guard-fold.ll b/llvm/test/CodeGen/AArch64/fptoi-nan-guard-fold.ll
index 1fba2b2f9647c..7c5a3e63feafe 100644
--- a/llvm/test/CodeGen/AArch64/fptoi-nan-guard-fold.ll
+++ b/llvm/test/CodeGen/AArch64/fptoi-nan-guard-fold.ll
@@ -75,17 +75,6 @@ define i32 @nan_guard_fptosi_f64(double %x) {
   ret i32 %sel
 }
 
-define i32 @nan_guard_fptosi_cmp_self(float %x) {
-; CHECK-LABEL: nan_guard_fptosi_cmp_self:
-; CHECK:       // %bb.0:
-; CHECK-NEXT:    fcvtzs w0, s0
-; CHECK-NEXT:    ret
-  %cmp = fcmp uno float %x, %x
-  %conv = fptosi float %x to i32
-  %sel = select i1 %cmp, i32 0, i32 %conv
-  ret i32 %sel
-}
-
 define <4 x i32> @nan_guard_fptosi_v4f32(<4 x float> %x) {
 ; CHECK-LABEL: nan_guard_fptosi_v4f32:
 ; CHECK:       // %bb.0:
diff --git a/llvm/test/CodeGen/AMDGPU/fptoi-nan-guard-fold.ll b/llvm/test/CodeGen/AMDGPU/fptoi-nan-guard-fold.ll
index a4334a4fb8ffc..81f86767e75fd 100644
--- a/llvm/test/CodeGen/AMDGPU/fptoi-nan-guard-fold.ll
+++ b/llvm/test/CodeGen/AMDGPU/fptoi-nan-guard-fold.ll
@@ -82,26 +82,22 @@ define i32 @nan_guard_fptosi_f64(double %x) {
   ret i32 %sel
 }
 
-define i32 @nan_guard_fptosi_cmp_self(float %x) {
-; CHECK-LABEL: nan_guard_fptosi_cmp_self:
-; CHECK:       ; %bb.0:
-; CHECK-NEXT:    s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
-; CHECK-NEXT:    v_cvt_i32_f32_e32 v0, v0
-; CHECK-NEXT:    s_setpc_b64 s[30:31]
-  %cmp = fcmp uno float %x, %x
-  %conv = fptosi float %x to i32
-  %sel = select i1 %cmp, i32 0, i32 %conv
-  ret i32 %sel
-}
-
 define <4 x i32> @nan_guard_fptosi_v4f32(<4 x float> %x) {
 ; CHECK-LABEL: nan_guard_fptosi_v4f32:
 ; CHECK:       ; %bb.0:
 ; CHECK-NEXT:    s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
-; CHECK-NEXT:    v_cvt_i32_f32_e32 v0, v0
-; CHECK-NEXT:    v_cvt_i32_f32_e32 v1, v1
-; CHECK-NEXT:    v_cvt_i32_f32_e32 v2, v2
-; CHECK-NEXT:    v_cvt_i32_f32_e32 v3, v3
+; CHECK-NEXT:    v_cvt_i32_f32_e32 v5, v0
+; CHECK-NEXT:    v_cvt_i32_f32_e32 v7, v1
+; CHECK-NEXT:    v_cvt_i32_f32_e32 v6, v2
+; CHECK-NEXT:    v_cvt_i32_f32_e32 v4, v3
+; CHECK-NEXT:    v_cmp_o_f32_e32 vcc, v0, v0
+; CHECK-NEXT:    v_cndmask_b32_e32 v0, 0, v5, vcc
+; CHECK-NEXT:    v_cmp_o_f32_e32 vcc, v1, v1
+; CHECK-NEXT:    v_cndmask_b32_e32 v1, 0, v7, vcc
+; CHECK-NEXT:    v_cmp_o_f32_e32 vcc, v2, v2
+; CHECK-NEXT:    v_cndmask_b32_e32 v2, 0, v6, vcc
+; CHECK-NEXT:    v_cmp_o_f32_e32 vcc, v3, v3
+; CHECK-NEXT:    v_cndmask_b32_e32 v3, 0, v4, vcc
 ; CHECK-NEXT:    s_setpc_b64 s[30:31]
   %cmp = fcmp uno <4 x float> %x, zeroinitializer
   %conv = fptosi <4 x float> %x to <4 x i32>

>From d0e1b85fee765e2b0a3a8f3a8d430e54e887008b Mon Sep 17 00:00:00 2001
From: Adel Ejjeh <adel.ejjeh at amd.com>
Date: Fri, 17 Jul 2026 10:48:35 -0500
Subject: [PATCH 3/3] [DAGCombiner] Address review: commutative AND matcher,
 freeze mask

Use a commutative m_And matcher for the optional mask on the guarded
conversion, and freeze the mask when reapplying it: for NaN inputs the
saturating conversion yields 0, so (and 0, Mask) must stay 0, which a
poison Mask would break. Expand the comment to list the matched forms,
and add tests for a non-constant mask, a commuted mask, and a
non-uno/ord compare predicate.

Assisted-by: Claude Code
---
 llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp | 34 +++++++-------
 .../CodeGen/AArch64/fptoi-nan-guard-fold.ll   | 42 +++++++++++++++++
 .../CodeGen/AMDGPU/fptoi-nan-guard-fold.ll    | 45 +++++++++++++++++++
 3 files changed, 106 insertions(+), 15 deletions(-)

diff --git a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
index 280ef9981e84b..ae57944079eb4 100644
--- a/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
+++ b/llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp
@@ -6289,9 +6289,13 @@ static SDValue performNanGuardFpToSatCombine(SDNode *N, SelectionDAG &DAG) {
   EVT VT = N->getValueType(0);
   SDLoc DL(N);
 
-  // Match an isnan-guarded select, requiring the compare to be single-use:
-  //   select (setcc X, 0.0, uno), 0, guarded
-  //   select (setcc X, 0.0, ord), guarded, 0
+  // Match an isnan-guarded select, requiring the compare to be single-use.
+  // The guarded value is fp_to_sint/fp_to_uint of X, optionally masked by an
+  // AND:
+  //   select (setcc X, 0.0, uno), 0, (fp_to_sint/uint X)
+  //   select (setcc X, 0.0, ord), (fp_to_sint/uint X), 0
+  //   select (setcc X, 0.0, uno), 0, (and (fp_to_sint/uint X), M)
+  //   select (setcc X, 0.0, ord), (and (fp_to_sint/uint X), M), 0
   SDValue X, GuardedVal;
   if (!sd_match(N,
                 m_SelectLike(m_OneUse(m_SetCC(m_Value(X), m_AnyZeroFP(),
@@ -6303,19 +6307,15 @@ static SDValue performNanGuardFpToSatCombine(SDNode *N, SelectionDAG &DAG) {
                              m_Value(GuardedVal), m_Zero())))
     return SDValue();
 
-  // Peel an optional AND mask, then require fp_to_sint/fp_to_uint of the same
-  // X.
+  // The guarded value must be fp_to_sint/fp_to_uint of the same X, optionally
+  // masked by a (commutative) AND.
   SDValue Mask;
-  SDValue Conv = GuardedVal;
-  if (Conv.getOpcode() == ISD::AND) {
-    Mask = Conv.getOperand(1);
-    Conv = Conv.getOperand(0);
-  }
-
   unsigned NewOpc;
-  if (sd_match(Conv, m_FPToSI(m_Specific(X))))
+  if (sd_match(GuardedVal, m_FPToSI(m_Specific(X))) ||
+      sd_match(GuardedVal, m_And(m_FPToSI(m_Specific(X)), m_Value(Mask))))
     NewOpc = ISD::FP_TO_SINT_SAT;
-  else if (sd_match(Conv, m_FPToUI(m_Specific(X))))
+  else if (sd_match(GuardedVal, m_FPToUI(m_Specific(X))) ||
+           sd_match(GuardedVal, m_And(m_FPToUI(m_Specific(X)), m_Value(Mask))))
     NewOpc = ISD::FP_TO_UINT_SAT;
   else
     return SDValue();
@@ -6326,8 +6326,12 @@ static SDValue performNanGuardFpToSatCombine(SDNode *N, SelectionDAG &DAG) {
 
   SDValue Sat =
       DAG.getNode(NewOpc, DL, VT, X, DAG.getValueType(VT.getScalarType()));
-  if (Mask)
-    Sat = DAG.getNode(ISD::AND, DL, VT, Sat, Mask);
+  if (Mask) {
+    // For NaN inputs the saturating conversion yields 0, so (and 0, Mask) must
+    // stay 0 to match the original select. A poison Mask would make it poison,
+    // so freeze Mask to guarantee a defined value.
+    Sat = DAG.getNode(ISD::AND, DL, VT, Sat, DAG.getFreeze(Mask));
+  }
   return Sat;
 }
 
diff --git a/llvm/test/CodeGen/AArch64/fptoi-nan-guard-fold.ll b/llvm/test/CodeGen/AArch64/fptoi-nan-guard-fold.ll
index 7c5a3e63feafe..e13ff98076800 100644
--- a/llvm/test/CodeGen/AArch64/fptoi-nan-guard-fold.ll
+++ b/llvm/test/CodeGen/AArch64/fptoi-nan-guard-fold.ll
@@ -64,6 +64,34 @@ define i32 @nan_guard_fptosi_and_mask(float %x) {
   ret i32 %sel
 }
 
+; AND mask with a non-constant operand.
+define i32 @nan_guard_fptosi_and_mask_var(float %x, i32 %m) {
+; CHECK-LABEL: nan_guard_fptosi_and_mask_var:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    fcvtzs w8, s0
+; CHECK-NEXT:    and w0, w8, w0
+; CHECK-NEXT:    ret
+  %cmp = fcmp uno float %x, 0.000000e+00
+  %conv = fptosi float %x to i32
+  %and = and i32 %conv, %m
+  %sel = select i1 %cmp, i32 0, i32 %and
+  ret i32 %sel
+}
+
+; AND mask with a non-constant operand, commuted.
+define i32 @nan_guard_fptosi_and_mask_commuted(float %x, i32 %m) {
+; CHECK-LABEL: nan_guard_fptosi_and_mask_commuted:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    fcvtzs w8, s0
+; CHECK-NEXT:    and w0, w8, w0
+; CHECK-NEXT:    ret
+  %cmp = fcmp uno float %x, 0.000000e+00
+  %conv = fptosi float %x to i32
+  %and = and i32 %m, %conv
+  %sel = select i1 %cmp, i32 0, i32 %and
+  ret i32 %sel
+}
+
 define i32 @nan_guard_fptosi_f64(double %x) {
 ; CHECK-LABEL: nan_guard_fptosi_f64:
 ; CHECK:       // %bb.0:
@@ -114,3 +142,17 @@ define i32 @nan_guard_mismatched_operands(float %x, float %y) {
   %sel = select i1 %cmp, i32 0, i32 %conv
   ret i32 %sel
 }
+
+; Compare is a different predicate (not uno/ord) -- should not fold.
+define i32 @nan_guard_wrong_predicate(float %x) {
+; CHECK-LABEL: nan_guard_wrong_predicate:
+; CHECK:       // %bb.0:
+; CHECK-NEXT:    fcvtzs w8, s0
+; CHECK-NEXT:    fcmp s0, #0.0
+; CHECK-NEXT:    csel w0, wzr, w8, mi
+; CHECK-NEXT:    ret
+  %cmp = fcmp olt float %x, 0.000000e+00
+  %conv = fptosi float %x to i32
+  %sel = select i1 %cmp, i32 0, i32 %conv
+  ret i32 %sel
+}
diff --git a/llvm/test/CodeGen/AMDGPU/fptoi-nan-guard-fold.ll b/llvm/test/CodeGen/AMDGPU/fptoi-nan-guard-fold.ll
index 81f86767e75fd..29e04eae1a90c 100644
--- a/llvm/test/CodeGen/AMDGPU/fptoi-nan-guard-fold.ll
+++ b/llvm/test/CodeGen/AMDGPU/fptoi-nan-guard-fold.ll
@@ -70,6 +70,36 @@ define i32 @nan_guard_fptosi_and_mask(float %x) {
   ret i32 %sel
 }
 
+; AND mask with a non-constant operand.
+define i32 @nan_guard_fptosi_and_mask_var(float %x, i32 %m) {
+; CHECK-LABEL: nan_guard_fptosi_and_mask_var:
+; CHECK:       ; %bb.0:
+; CHECK-NEXT:    s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+; CHECK-NEXT:    v_cvt_i32_f32_e32 v0, v0
+; CHECK-NEXT:    v_and_b32_e32 v0, v0, v1
+; CHECK-NEXT:    s_setpc_b64 s[30:31]
+  %cmp = fcmp uno float %x, 0.000000e+00
+  %conv = fptosi float %x to i32
+  %and = and i32 %conv, %m
+  %sel = select i1 %cmp, i32 0, i32 %and
+  ret i32 %sel
+}
+
+; AND mask with a non-constant operand, commuted.
+define i32 @nan_guard_fptosi_and_mask_commuted(float %x, i32 %m) {
+; CHECK-LABEL: nan_guard_fptosi_and_mask_commuted:
+; CHECK:       ; %bb.0:
+; CHECK-NEXT:    s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+; CHECK-NEXT:    v_cvt_i32_f32_e32 v0, v0
+; CHECK-NEXT:    v_and_b32_e32 v0, v0, v1
+; CHECK-NEXT:    s_setpc_b64 s[30:31]
+  %cmp = fcmp uno float %x, 0.000000e+00
+  %conv = fptosi float %x to i32
+  %and = and i32 %m, %conv
+  %sel = select i1 %cmp, i32 0, i32 %and
+  ret i32 %sel
+}
+
 define i32 @nan_guard_fptosi_f64(double %x) {
 ; CHECK-LABEL: nan_guard_fptosi_f64:
 ; CHECK:       ; %bb.0:
@@ -134,3 +164,18 @@ define i32 @nan_guard_mismatched_operands(float %x, float %y) {
   %sel = select i1 %cmp, i32 0, i32 %conv
   ret i32 %sel
 }
+
+; Compare is a different predicate (not uno/ord) -- should not fold.
+define i32 @nan_guard_wrong_predicate(float %x) {
+; CHECK-LABEL: nan_guard_wrong_predicate:
+; CHECK:       ; %bb.0:
+; CHECK-NEXT:    s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+; CHECK-NEXT:    v_cvt_i32_f32_e32 v1, v0
+; CHECK-NEXT:    v_cmp_ngt_f32_e32 vcc, 0, v0
+; CHECK-NEXT:    v_cndmask_b32_e32 v0, 0, v1, vcc
+; CHECK-NEXT:    s_setpc_b64 s[30:31]
+  %cmp = fcmp olt float %x, 0.000000e+00
+  %conv = fptosi float %x to i32
+  %sel = select i1 %cmp, i32 0, i32 %conv
+  ret i32 %sel
+}



More information about the llvm-commits mailing list