[llvm] [InstCombine] Canonicalize nsz NaN-fallback float clamp to minnum/maxnum (PR #213690)

Jun Yeong Kim via llvm-commits llvm-commits at lists.llvm.org
Fri Aug 28 19:33:03 PDT 2026


https://github.com/junyeong0619 updated https://github.com/llvm/llvm-project/pull/213690

>From 0e5281be84a96f059d2074bb96d56abada64f1ab Mon Sep 17 00:00:00 2001
From: Jun Yeong Kim <junyeonggim5 at gmail.com>
Date: Fri, 7 Aug 2026 07:41:24 +0900
Subject: [PATCH 1/2] [InstCombine] Add baseline tests for nsz NaN-fallback
 float clamp

---
 .../InstCombine/clamp-nan-fallback.ll         | 286 ++++++++++++++++++
 1 file changed, 286 insertions(+)
 create mode 100644 llvm/test/Transforms/InstCombine/clamp-nan-fallback.ll

diff --git a/llvm/test/Transforms/InstCombine/clamp-nan-fallback.ll b/llvm/test/Transforms/InstCombine/clamp-nan-fallback.ll
new file mode 100644
index 0000000000000..ee1c9e0db3888
--- /dev/null
+++ b/llvm/test/Transforms/InstCombine/clamp-nan-fallback.ll
@@ -0,0 +1,286 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; RUN: opt -passes=instcombine -S < %s | FileCheck %s
+
+declare float @llvm.minnum.f32(float, float)
+declare float @llvm.maxnum.f32(float, float)
+
+; ---- POS: exact clamp + NaN-fallback pattern (folds) ----
+define float @pos_scalar(float %x, float %fallback) {
+; CHECK-LABEL: define float @pos_scalar(
+; CHECK-SAME: float [[X:%.*]], float [[FALLBACK:%.*]]) {
+; CHECK-NEXT:    [[BELOW_LO_OR_NAN:%.*]] = fcmp nsz ult float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[BELOW_HI:%.*]] = fcmp nsz olt float [[X]], 1.000000e+00
+; CHECK-NEXT:    [[UPPER:%.*]] = select nsz i1 [[BELOW_HI]], float [[X]], float 1.000000e+00
+; CHECK-NEXT:    [[ORDERED:%.*]] = fcmp ord float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[LOWER_OR_FALLBACK:%.*]] = select nsz i1 [[ORDERED]], float 0.000000e+00, float [[FALLBACK]]
+; CHECK-NEXT:    [[RESULT:%.*]] = select nsz i1 [[BELOW_LO_OR_NAN]], float [[LOWER_OR_FALLBACK]], float [[UPPER]]
+; CHECK-NEXT:    ret float [[RESULT]]
+;
+  %below_lo_or_nan = fcmp nsz ult float %x, 0.0
+  %below_hi = fcmp nsz olt float %x, 1.0
+  %upper = select nsz i1 %below_hi, float %x, float 1.0
+  %ordered = fcmp ord float %x, 0.0
+  %lower_or_fallback = select nsz i1 %ordered, float 0.0, float %fallback
+  %result = select nsz i1 %below_lo_or_nan, float %lower_or_fallback, float %upper
+  ret float %result
+}
+
+; ---- POS vector: fixed <8 x float> clamp+NaN-fallback ----
+define <8 x float> @pos_vector(<8 x float> %x, <8 x float> %fallback) {
+; CHECK-LABEL: define <8 x float> @pos_vector(
+; CHECK-SAME: <8 x float> [[X:%.*]], <8 x float> [[FALLBACK:%.*]]) {
+; CHECK-NEXT:    [[BELOW_LO_OR_NAN:%.*]] = fcmp nsz ult <8 x float> [[X]], zeroinitializer
+; CHECK-NEXT:    [[BELOW_HI:%.*]] = fcmp nsz olt <8 x float> [[X]], splat (float 1.000000e+00)
+; CHECK-NEXT:    [[UPPER:%.*]] = select nsz <8 x i1> [[BELOW_HI]], <8 x float> [[X]], <8 x float> splat (float 1.000000e+00)
+; CHECK-NEXT:    [[ORDERED:%.*]] = fcmp ord <8 x float> [[X]], zeroinitializer
+; CHECK-NEXT:    [[LOWER_OR_FALLBACK:%.*]] = select nsz <8 x i1> [[ORDERED]], <8 x float> zeroinitializer, <8 x float> [[FALLBACK]]
+; CHECK-NEXT:    [[CLAMP_OR_FALLBACK:%.*]] = select nsz <8 x i1> [[BELOW_LO_OR_NAN]], <8 x float> [[LOWER_OR_FALLBACK]], <8 x float> [[UPPER]]
+; CHECK-NEXT:    ret <8 x float> [[CLAMP_OR_FALLBACK]]
+;
+  %below_lo_or_nan = fcmp nsz ult <8 x float> %x, splat (float 0.0)
+  %below_hi = fcmp nsz olt <8 x float> %x, splat (float 1.0)
+  %upper = select nsz <8 x i1> %below_hi, <8 x float> %x, <8 x float> splat (float 1.0)
+  %ordered = fcmp ord <8 x float> %x, splat (float 0.0)
+  %lower_or_fallback = select nsz <8 x i1> %ordered, <8 x float> splat (float 0.0), <8 x float> %fallback
+  %result = select nsz <8 x i1> %below_lo_or_nan, <8 x float> %lower_or_fallback, <8 x float> %upper
+  ret <8 x float> %result
+}
+
+; ---- POS no-fcmp-nsz: nsz on the fcmps is not required (meaningless) ----
+define float @pos_no_fcmp_nsz(float %x, float %fallback) {
+; CHECK-LABEL: define float @pos_no_fcmp_nsz(
+; CHECK-SAME: float [[X:%.*]], float [[FALLBACK:%.*]]) {
+; CHECK-NEXT:    [[BELOW_LO_OR_NAN:%.*]] = fcmp ult float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[BELOW_HI:%.*]] = fcmp olt float [[X]], 1.000000e+00
+; CHECK-NEXT:    [[UPPER:%.*]] = select nsz i1 [[BELOW_HI]], float [[X]], float 1.000000e+00
+; CHECK-NEXT:    [[ORDERED:%.*]] = fcmp ord float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[LOWER_OR_FALLBACK:%.*]] = select nsz i1 [[ORDERED]], float 0.000000e+00, float [[FALLBACK]]
+; CHECK-NEXT:    [[RESULT:%.*]] = select nsz i1 [[BELOW_LO_OR_NAN]], float [[LOWER_OR_FALLBACK]], float [[UPPER]]
+; CHECK-NEXT:    ret float [[RESULT]]
+;
+  %below_lo_or_nan = fcmp ult float %x, 0.0
+  %below_hi = fcmp olt float %x, 1.0
+  %upper = select nsz i1 %below_hi, float %x, float 1.0
+  %ordered = fcmp ord float %x, 0.0
+  %lower_or_fallback = select nsz i1 %ordered, float 0.0, float %fallback
+  %result = select nsz i1 %below_lo_or_nan, float %lower_or_fallback, float %upper
+  ret float %result
+}
+
+; ---- N1: outer select missing nsz ----
+define float @n1_outer_no_nsz(float %x, float %fallback) {
+; CHECK-LABEL: define float @n1_outer_no_nsz(
+; CHECK-SAME: float [[X:%.*]], float [[FALLBACK:%.*]]) {
+; CHECK-NEXT:    [[BELOW_LO_OR_NAN:%.*]] = fcmp nsz ult float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[BELOW_HI:%.*]] = fcmp nsz olt float [[X]], 1.000000e+00
+; CHECK-NEXT:    [[UPPER:%.*]] = select nsz i1 [[BELOW_HI]], float [[X]], float 1.000000e+00
+; CHECK-NEXT:    [[ORDERED:%.*]] = fcmp ord float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[LOWER_OR_FALLBACK:%.*]] = select nsz i1 [[ORDERED]], float 0.000000e+00, float [[FALLBACK]]
+; CHECK-NEXT:    [[RESULT:%.*]] = select i1 [[BELOW_LO_OR_NAN]], float [[LOWER_OR_FALLBACK]], float [[UPPER]]
+; CHECK-NEXT:    ret float [[RESULT]]
+;
+  %below_lo_or_nan = fcmp nsz ult float %x, 0.0
+  %below_hi = fcmp nsz olt float %x, 1.0
+  %upper = select nsz i1 %below_hi, float %x, float 1.0
+  %ordered = fcmp ord float %x, 0.0
+  %lower_or_fallback = select nsz i1 %ordered, float 0.0, float %fallback
+  %result = select i1 %below_lo_or_nan, float %lower_or_fallback, float %upper
+  ret float %result
+}
+
+; ---- N2: upper select missing nsz ----
+define float @n2_upper_no_nsz(float %x, float %fallback) {
+; CHECK-LABEL: define float @n2_upper_no_nsz(
+; CHECK-SAME: float [[X:%.*]], float [[FALLBACK:%.*]]) {
+; CHECK-NEXT:    [[BELOW_LO_OR_NAN:%.*]] = fcmp nsz ult float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[BELOW_HI:%.*]] = fcmp nsz olt float [[X]], 1.000000e+00
+; CHECK-NEXT:    [[UPPER:%.*]] = select i1 [[BELOW_HI]], float [[X]], float 1.000000e+00
+; CHECK-NEXT:    [[ORDERED:%.*]] = fcmp ord float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[LOWER_OR_FALLBACK:%.*]] = select nsz i1 [[ORDERED]], float 0.000000e+00, float [[FALLBACK]]
+; CHECK-NEXT:    [[RESULT:%.*]] = select nsz i1 [[BELOW_LO_OR_NAN]], float [[LOWER_OR_FALLBACK]], float [[UPPER]]
+; CHECK-NEXT:    ret float [[RESULT]]
+;
+  %below_lo_or_nan = fcmp nsz ult float %x, 0.0
+  %below_hi = fcmp nsz olt float %x, 1.0
+  %upper = select i1 %below_hi, float %x, float 1.0
+  %ordered = fcmp ord float %x, 0.0
+  %lower_or_fallback = select nsz i1 %ordered, float 0.0, float %fallback
+  %result = select nsz i1 %below_lo_or_nan, float %lower_or_fallback, float %upper
+  ret float %result
+}
+
+; ---- N3: ult -> olt ----
+define float @n3_ult_to_olt(float %x, float %fallback) {
+; CHECK-LABEL: define float @n3_ult_to_olt(
+; CHECK-SAME: float [[X:%.*]], float [[FALLBACK:%.*]]) {
+; CHECK-NEXT:    [[BELOW_LO_OR_NAN:%.*]] = fcmp nsz olt float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[BELOW_HI:%.*]] = fcmp nsz olt float [[X]], 1.000000e+00
+; CHECK-NEXT:    [[UPPER:%.*]] = select nsz i1 [[BELOW_HI]], float [[X]], float 1.000000e+00
+; CHECK-NEXT:    [[RESULT:%.*]] = select nsz i1 [[BELOW_LO_OR_NAN]], float 0.000000e+00, float [[UPPER]]
+; CHECK-NEXT:    ret float [[RESULT]]
+;
+  %below_lo_or_nan = fcmp nsz olt float %x, 0.0
+  %below_hi = fcmp nsz olt float %x, 1.0
+  %upper = select nsz i1 %below_hi, float %x, float 1.0
+  %ordered = fcmp ord float %x, 0.0
+  %lower_or_fallback = select nsz i1 %ordered, float 0.0, float %fallback
+  %result = select nsz i1 %below_lo_or_nan, float %lower_or_fallback, float %upper
+  ret float %result
+}
+
+; ---- N4: lower-bound constant mismatch (0.0 -> 2.0) ----
+define float @n4_lo_mismatch(float %x, float %fallback) {
+; CHECK-LABEL: define float @n4_lo_mismatch(
+; CHECK-SAME: float [[X:%.*]], float [[FALLBACK:%.*]]) {
+; CHECK-NEXT:    [[BELOW_LO_OR_NAN:%.*]] = fcmp nsz ult float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[BELOW_HI:%.*]] = fcmp nsz olt float [[X]], 1.000000e+00
+; CHECK-NEXT:    [[UPPER:%.*]] = select nsz i1 [[BELOW_HI]], float [[X]], float 1.000000e+00
+; CHECK-NEXT:    [[ORDERED:%.*]] = fcmp ord float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[LOWER_OR_FALLBACK:%.*]] = select nsz i1 [[ORDERED]], float 2.000000e+00, float [[FALLBACK]]
+; CHECK-NEXT:    [[RESULT:%.*]] = select nsz i1 [[BELOW_LO_OR_NAN]], float [[LOWER_OR_FALLBACK]], float [[UPPER]]
+; CHECK-NEXT:    ret float [[RESULT]]
+;
+  %below_lo_or_nan = fcmp nsz ult float %x, 0.0
+  %below_hi = fcmp nsz olt float %x, 1.0
+  %upper = select nsz i1 %below_hi, float %x, float 1.0
+  %ordered = fcmp ord float %x, 0.0
+  %lower_or_fallback = select nsz i1 %ordered, float 2.0, float %fallback
+  %result = select nsz i1 %below_lo_or_nan, float %lower_or_fallback, float %upper
+  ret float %result
+}
+
+; ---- N5: compared operand mismatch (%x -> %y) ----
+define float @n5_operand_mismatch(float %x, float %y, float %fallback) {
+; CHECK-LABEL: define float @n5_operand_mismatch(
+; CHECK-SAME: float [[X:%.*]], float [[Y:%.*]], float [[FALLBACK:%.*]]) {
+; CHECK-NEXT:    [[BELOW_LO_OR_NAN:%.*]] = fcmp nsz ult float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[BELOW_HI:%.*]] = fcmp nsz olt float [[Y]], 1.000000e+00
+; CHECK-NEXT:    [[UPPER:%.*]] = select nsz i1 [[BELOW_HI]], float [[X]], float 1.000000e+00
+; CHECK-NEXT:    [[ORDERED:%.*]] = fcmp ord float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[LOWER_OR_FALLBACK:%.*]] = select nsz i1 [[ORDERED]], float 0.000000e+00, float [[FALLBACK]]
+; CHECK-NEXT:    [[RESULT:%.*]] = select nsz i1 [[BELOW_LO_OR_NAN]], float [[LOWER_OR_FALLBACK]], float [[UPPER]]
+; CHECK-NEXT:    ret float [[RESULT]]
+;
+  %below_lo_or_nan = fcmp nsz ult float %x, 0.0
+  %below_hi = fcmp nsz olt float %y, 1.0
+  %upper = select nsz i1 %below_hi, float %x, float 1.0
+  %ordered = fcmp ord float %x, 0.0
+  %lower_or_fallback = select nsz i1 %ordered, float 0.0, float %fallback
+  %result = select nsz i1 %below_lo_or_nan, float %lower_or_fallback, float %upper
+  ret float %result
+}
+
+; ---- N6: ord -> uno ----
+define float @n6_ord_to_uno(float %x, float %fallback) {
+; CHECK-LABEL: define float @n6_ord_to_uno(
+; CHECK-SAME: float [[X:%.*]], float [[FALLBACK:%.*]]) {
+; CHECK-NEXT:    [[BELOW_LO_OR_NAN:%.*]] = fcmp nsz ult float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[BELOW_HI:%.*]] = fcmp nsz olt float [[X]], 1.000000e+00
+; CHECK-NEXT:    [[UPPER:%.*]] = select nsz i1 [[BELOW_HI]], float [[X]], float 1.000000e+00
+; CHECK-NEXT:    [[ORDERED:%.*]] = fcmp uno float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[LOWER_OR_FALLBACK:%.*]] = select nsz i1 [[ORDERED]], float 0.000000e+00, float [[FALLBACK]]
+; CHECK-NEXT:    [[RESULT:%.*]] = select nsz i1 [[BELOW_LO_OR_NAN]], float [[LOWER_OR_FALLBACK]], float [[UPPER]]
+; CHECK-NEXT:    ret float [[RESULT]]
+;
+  %below_lo_or_nan = fcmp nsz ult float %x, 0.0
+  %below_hi = fcmp nsz olt float %x, 1.0
+  %upper = select nsz i1 %below_hi, float %x, float 1.0
+  %ordered = fcmp uno float %x, 0.0
+  %lower_or_fallback = select nsz i1 %ordered, float 0.0, float %fallback
+  %result = select nsz i1 %below_lo_or_nan, float %lower_or_fallback, float %upper
+  ret float %result
+}
+
+; ---- N7: non-ieee denormal-fp-math (otherwise same as POS) ----
+define float @n7_non_ieee_denormal(float %x, float %fallback) denormal_fpenv(preservesign) {
+; CHECK-LABEL: define float @n7_non_ieee_denormal(
+; CHECK-SAME: float [[X:%.*]], float [[FALLBACK:%.*]]) #[[ATTR1:[0-9]+]] {
+; CHECK-NEXT:    [[BELOW_LO_OR_NAN:%.*]] = fcmp nsz ult float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[BELOW_HI:%.*]] = fcmp nsz olt float [[X]], 1.000000e+00
+; CHECK-NEXT:    [[UPPER:%.*]] = select nsz i1 [[BELOW_HI]], float [[X]], float 1.000000e+00
+; CHECK-NEXT:    [[ORDERED:%.*]] = fcmp ord float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[LOWER_OR_FALLBACK:%.*]] = select nsz i1 [[ORDERED]], float 0.000000e+00, float [[FALLBACK]]
+; CHECK-NEXT:    [[RESULT:%.*]] = select nsz i1 [[BELOW_LO_OR_NAN]], float [[LOWER_OR_FALLBACK]], float [[UPPER]]
+; CHECK-NEXT:    ret float [[RESULT]]
+;
+  %below_lo_or_nan = fcmp nsz ult float %x, 0.0
+  %below_hi = fcmp nsz olt float %x, 1.0
+  %upper = select nsz i1 %below_hi, float %x, float 1.0
+  %ordered = fcmp ord float %x, 0.0
+  %lower_or_fallback = select nsz i1 %ordered, float 0.0, float %fallback
+  %result = select nsz i1 %below_lo_or_nan, float %lower_or_fallback, float %upper
+  ret float %result
+}
+; ---- N8: intermediate %upper has an extra use -> not one-use, no fold ----
+define float @n8_multi_use(float %x, float %fallback, ptr %p) {
+; CHECK-LABEL: define float @n8_multi_use(
+; CHECK-SAME: float [[X:%.*]], float [[FALLBACK:%.*]], ptr [[P:%.*]]) {
+; CHECK-NEXT:    [[BELOW_LO_OR_NAN:%.*]] = fcmp nsz ult float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[BELOW_HI:%.*]] = fcmp nsz olt float [[X]], 1.000000e+00
+; CHECK-NEXT:    [[UPPER:%.*]] = select nsz i1 [[BELOW_HI]], float [[X]], float 1.000000e+00
+; CHECK-NEXT:    store float [[UPPER]], ptr [[P]], align 4
+; CHECK-NEXT:    [[ORDERED:%.*]] = fcmp ord float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[LOWER_OR_FALLBACK:%.*]] = select nsz i1 [[ORDERED]], float 0.000000e+00, float [[FALLBACK]]
+; CHECK-NEXT:    [[RESULT:%.*]] = select nsz i1 [[BELOW_LO_OR_NAN]], float [[LOWER_OR_FALLBACK]], float [[UPPER]]
+; CHECK-NEXT:    ret float [[RESULT]]
+;
+  %below_lo_or_nan = fcmp nsz ult float %x, 0.0
+  %below_hi = fcmp nsz olt float %x, 1.0
+  %upper = select nsz i1 %below_hi, float %x, float 1.0
+  store float %upper, ptr %p
+  %ordered = fcmp ord float %x, 0.0
+  %lower_or_fallback = select nsz i1 %ordered, float 0.0, float %fallback
+  %result = select nsz i1 %below_lo_or_nan, float %lower_or_fallback, float %upper
+  ret float %result
+}
+
+; ---- N9: LO > HI (ill-formed clamp), must not fold ----
+define float @n9_lo_gt_hi(float %x, float %fallback) {
+; CHECK-LABEL: define float @n9_lo_gt_hi(
+; CHECK-SAME: float [[X:%.*]], float [[FALLBACK:%.*]]) {
+; CHECK-NEXT:    [[BELOW_LO_OR_NAN:%.*]] = fcmp nsz ult float [[X]], 1.000000e+00
+; CHECK-NEXT:    [[ORDERED:%.*]] = fcmp ord float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[LOWER_OR_FALLBACK:%.*]] = select nsz i1 [[ORDERED]], float 1.000000e+00, float [[FALLBACK]]
+; CHECK-NEXT:    [[RESULT:%.*]] = select nsz i1 [[BELOW_LO_OR_NAN]], float [[LOWER_OR_FALLBACK]], float 0.000000e+00
+; CHECK-NEXT:    ret float [[RESULT]]
+;
+  %below_lo_or_nan = fcmp nsz ult float %x, 1.0
+  %below_hi = fcmp nsz olt float %x, 0.0
+  %upper = select nsz i1 %below_hi, float %x, float 0.0
+  %ordered = fcmp ord float %x, 1.0
+  %lower_or_fallback = select nsz i1 %ordered, float 1.0, float %fallback
+  %result = select nsz i1 %below_lo_or_nan, float %lower_or_fallback, float %upper
+  ret float %result
+}
+
+; ---- N10: lower select missing nsz ----
+define float @n10_lower_no_nsz(float %x, float %fallback) {
+; CHECK-LABEL: define float @n10_lower_no_nsz(
+; CHECK-SAME: float [[X:%.*]], float [[FALLBACK:%.*]]) {
+; CHECK-NEXT:    [[BELOW_LO_OR_NAN:%.*]] = fcmp nsz ult float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[BELOW_HI:%.*]] = fcmp nsz olt float [[X]], 1.000000e+00
+; CHECK-NEXT:    [[UPPER:%.*]] = select nsz i1 [[BELOW_HI]], float [[X]], float 1.000000e+00
+; CHECK-NEXT:    [[ORDERED:%.*]] = fcmp ord float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[LOWER_OR_FALLBACK:%.*]] = select i1 [[ORDERED]], float 0.000000e+00, float [[FALLBACK]]
+; CHECK-NEXT:    [[RESULT:%.*]] = select nsz i1 [[BELOW_LO_OR_NAN]], float [[LOWER_OR_FALLBACK]], float [[UPPER]]
+; CHECK-NEXT:    ret float [[RESULT]]
+;
+  %below_lo_or_nan = fcmp nsz ult float %x, 0.0
+  %below_hi = fcmp nsz olt float %x, 1.0
+  %upper = select nsz i1 %below_hi, float %x, float 1.0
+  %ordered = fcmp ord float %x, 0.0
+  %lower_or_fallback = select i1 %ordered, float 0.0, float %fallback
+  %result = select nsz i1 %below_lo_or_nan, float %lower_or_fallback, float %upper
+  ret float %result
+}
+
+; ---- N11: select of homogeneous FP aggregate must not crash / not fold ----
+define { float, float } @n11_fp_aggregate(i1 %c, { float, float } %a, { float, float } %b) {
+; CHECK-LABEL: define { float, float } @n11_fp_aggregate(
+; CHECK-SAME: i1 [[C:%.*]], { float, float } [[A:%.*]], { float, float } [[B:%.*]]) {
+; CHECK-NEXT:    [[R:%.*]] = select nsz i1 [[C]], { float, float } [[A]], { float, float } [[B]]
+; CHECK-NEXT:    ret { float, float } [[R]]
+;
+  %r = select nsz i1 %c, { float, float } %a, { float, float } %b
+  ret { float, float } %r
+}

>From a388f72996ba435be367e0f939198dc778d8f553 Mon Sep 17 00:00:00 2001
From: Jun Yeong Kim <junyeonggim5 at gmail.com>
Date: Fri, 7 Aug 2026 07:47:35 +0900
Subject: [PATCH 2/2] [InstCombine] Canonicalize nsz NaN-fallback float clamp
 to minnum/maxnum

Fold the fcmp/select clamp-to-[LO,HI]-with-NaN-fallback tree into
llvm.minnum/llvm.maxnum plus an explicit NaN select, when every
value-producing select is nsz and the function uses IEEE denormals.
The matcher is deliberately narrow (exact predicates/operands/bounds,
LO <= HI, one-use intermediates) and never infers nnan/ninf. nsz on the
fcmps is not required (fcmp already treats +0.0 and -0.0 as equal). Only
scalar and vector float selects are handled, not FP aggregates.

Fixes #213189
---
 .../InstCombine/InstCombineSelect.cpp         | 99 +++++++++++++++++++
 .../InstCombine/clamp-nan-fallback.ll         | 30 +++---
 2 files changed, 111 insertions(+), 18 deletions(-)

diff --git a/llvm/lib/Transforms/InstCombine/InstCombineSelect.cpp b/llvm/lib/Transforms/InstCombine/InstCombineSelect.cpp
index 090abfaea28ec..1114b0735c5ed 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineSelect.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineSelect.cpp
@@ -4582,6 +4582,102 @@ static bool isSelectZeroSignInsignificant(SelectInst &SI) {
   return true;
 }
 
+/// Canonicalize an `nsz` NaN-fallback float clamp to `minnum`/`maxnum`:
+///
+///   below_lo_or_nan = fcmp ult %x, LO
+///   below_hi        = fcmp olt %x, HI
+///   upper           = select nsz below_hi, %x, HI
+///   ordered         = fcmp ord %x, LO
+///   lower_or_fb     = select nsz ordered, LO, %fallback
+///   result          = select nsz below_lo_or_nan, lower_or_fb, upper
+///
+/// =>
+///
+///   isnan   = fcmp uno %x, %x
+///   upperV  = call nsz @llvm.minnum(%x, HI)
+///   clamped = call nsz @llvm.maxnum(upperV, LO)
+///   result  = select isnan, %fallback, clamped
+///
+/// The match is deliberately narrow: exact predicates/operands/bounds, `nsz` on
+/// every value-producing select, `denormal-fp-math = ieee`, and one-use
+/// intermediate nodes so the original tree is fully removed. `nnan`/`ninf` are
+/// never inferred; NaN is handled explicitly.
+static Value *foldClampWithNaNFallback(SelectInst &Outer,
+                                       InstCombiner::BuilderTy &Builder) {
+  // Only handle scalar/vector float selects. FPMathOperator also covers selects
+  // of homogeneous FP aggregates (e.g. { float, float }), whose scalar type has
+  // no fltSemantics, so restrict to isFPOrFPVectorTy() before querying
+  // fast-math flags or FP semantics below.
+  if (!Outer.getType()->isFPOrFPVectorTy() || !Outer.hasNoSignedZeros())
+    return nullptr;
+
+  // The minnum/maxnum form treats denormals per IEEE. If the function may flush
+  // denormals to zero (FTZ/DAZ, e.g. from -ffast-math / crtfastmath.o), the
+  // rewrite is not value-equivalent.
+  Type *Ty = Outer.getType()->getScalarType();
+  const fltSemantics &FPSem = Ty->getFltSemantics();
+  if (Outer.getFunction()->getDenormalMode(FPSem) != DenormalMode::getIEEE())
+    return nullptr;
+
+  Value *BelowLoOrNan, *LowerOrFallback, *Upper;
+  if (!match(&Outer, m_Select(m_Value(BelowLoOrNan), m_Value(LowerOrFallback),
+                              m_Value(Upper))))
+    return nullptr;
+
+  // belowLoOrNan: fcmp nsz ult %x, LO
+  auto *Cmp1 = dyn_cast<FCmpInst>(BelowLoOrNan);
+  // nsz on the fcmp is meaningless (fcmp already treats +0.0 and -0.0 as
+  // equal), so it is not required here.
+  if (!Cmp1 || !Cmp1->hasOneUse() || Cmp1->getPredicate() != FCmpInst::FCMP_ULT)
+    return nullptr;
+  Value *X = Cmp1->getOperand(0);
+  auto *Lo = dyn_cast<Constant>(Cmp1->getOperand(1));
+  if (!Lo)
+    return nullptr;
+
+  // upper: select nsz (fcmp olt %x, HI), %x, HI
+  auto *UpperSel = dyn_cast<SelectInst>(Upper);
+  if (!UpperSel || !UpperSel->hasOneUse() || !UpperSel->hasNoSignedZeros())
+    return nullptr;
+  auto *Cmp2 = dyn_cast<FCmpInst>(UpperSel->getCondition());
+  if (!Cmp2 || !Cmp2->hasOneUse() ||
+      Cmp2->getPredicate() != FCmpInst::FCMP_OLT || Cmp2->getOperand(0) != X)
+    return nullptr;
+  auto *Hi = dyn_cast<Constant>(Cmp2->getOperand(1));
+  if (!Hi || UpperSel->getTrueValue() != X || UpperSel->getFalseValue() != Hi)
+    return nullptr;
+
+  // lowerOrFallback: select nsz (fcmp ord %x, *), LO, %fallback
+  auto *LowSel = dyn_cast<SelectInst>(LowerOrFallback);
+  if (!LowSel || !LowSel->hasOneUse() || !LowSel->hasNoSignedZeros())
+    return nullptr;
+  auto *Cmp3 = dyn_cast<FCmpInst>(LowSel->getCondition());
+  if (!Cmp3 || !Cmp3->hasOneUse() ||
+      Cmp3->getPredicate() != FCmpInst::FCMP_ORD || Cmp3->getOperand(0) != X)
+    return nullptr;
+  if (LowSel->getTrueValue() != Lo)
+    return nullptr;
+  Value *Fallback = LowSel->getFalseValue();
+
+  // Require LO <= HI (as constants) so the clamp is well-formed. For LO > HI
+  // the source tree and the minnum/maxnum form disagree, so bail out.
+  Constant *LeCmp = ConstantFoldCompareInstOperands(FCmpInst::FCMP_OLE, Lo, Hi,
+                                                    Outer.getDataLayout());
+  if (!LeCmp || !match(LeCmp, m_One()))
+    return nullptr;
+
+  // Match succeeded -- build the normalized form.
+  Value *IsNaN = Builder.CreateFCmpUNO(X, X, "isnan");
+
+  Value *UpperV = Builder.CreateBinaryIntrinsic(Intrinsic::minnum, X, Hi);
+  cast<Instruction>(UpperV)->setHasNoSignedZeros(true);
+
+  Value *Clamped = Builder.CreateBinaryIntrinsic(Intrinsic::maxnum, UpperV, Lo);
+  cast<Instruction>(Clamped)->setHasNoSignedZeros(true);
+
+  return Builder.CreateSelect(IsNaN, Fallback, Clamped, "clamp.or.fallback");
+}
+
 Instruction *InstCombinerImpl::visitSelectInst(SelectInst &SI) {
   Value *CondVal = SI.getCondition();
   Value *TrueVal = SI.getTrueValue();
@@ -5417,5 +5513,8 @@ Instruction *InstCombinerImpl::visitSelectInst(SelectInst &SI) {
     return CallInst::Create(Scmp, {CmpLHS, ConstantInt::get(SI.getType(), 0)});
   }
 
+  if (Value *V = foldClampWithNaNFallback(SI, Builder))
+    return replaceInstUsesWith(SI, V);
+
   return nullptr;
 }
diff --git a/llvm/test/Transforms/InstCombine/clamp-nan-fallback.ll b/llvm/test/Transforms/InstCombine/clamp-nan-fallback.ll
index ee1c9e0db3888..52a0e01416ca1 100644
--- a/llvm/test/Transforms/InstCombine/clamp-nan-fallback.ll
+++ b/llvm/test/Transforms/InstCombine/clamp-nan-fallback.ll
@@ -8,12 +8,10 @@ declare float @llvm.maxnum.f32(float, float)
 define float @pos_scalar(float %x, float %fallback) {
 ; CHECK-LABEL: define float @pos_scalar(
 ; CHECK-SAME: float [[X:%.*]], float [[FALLBACK:%.*]]) {
-; CHECK-NEXT:    [[BELOW_LO_OR_NAN:%.*]] = fcmp nsz ult float [[X]], 0.000000e+00
-; CHECK-NEXT:    [[BELOW_HI:%.*]] = fcmp nsz olt float [[X]], 1.000000e+00
-; CHECK-NEXT:    [[UPPER:%.*]] = select nsz i1 [[BELOW_HI]], float [[X]], float 1.000000e+00
-; CHECK-NEXT:    [[ORDERED:%.*]] = fcmp ord float [[X]], 0.000000e+00
-; CHECK-NEXT:    [[LOWER_OR_FALLBACK:%.*]] = select nsz i1 [[ORDERED]], float 0.000000e+00, float [[FALLBACK]]
-; CHECK-NEXT:    [[RESULT:%.*]] = select nsz i1 [[BELOW_LO_OR_NAN]], float [[LOWER_OR_FALLBACK]], float [[UPPER]]
+; CHECK-NEXT:    [[ISNAN:%.*]] = fcmp uno float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[TMP1:%.*]] = call nsz float @llvm.minnum.f32(float [[X]], float 1.000000e+00)
+; CHECK-NEXT:    [[TMP2:%.*]] = call nsz float @llvm.maxnum.f32(float [[TMP1]], float 0.000000e+00)
+; CHECK-NEXT:    [[RESULT:%.*]] = select i1 [[ISNAN]], float [[FALLBACK]], float [[TMP2]]
 ; CHECK-NEXT:    ret float [[RESULT]]
 ;
   %below_lo_or_nan = fcmp nsz ult float %x, 0.0
@@ -29,12 +27,10 @@ define float @pos_scalar(float %x, float %fallback) {
 define <8 x float> @pos_vector(<8 x float> %x, <8 x float> %fallback) {
 ; CHECK-LABEL: define <8 x float> @pos_vector(
 ; CHECK-SAME: <8 x float> [[X:%.*]], <8 x float> [[FALLBACK:%.*]]) {
-; CHECK-NEXT:    [[BELOW_LO_OR_NAN:%.*]] = fcmp nsz ult <8 x float> [[X]], zeroinitializer
-; CHECK-NEXT:    [[BELOW_HI:%.*]] = fcmp nsz olt <8 x float> [[X]], splat (float 1.000000e+00)
-; CHECK-NEXT:    [[UPPER:%.*]] = select nsz <8 x i1> [[BELOW_HI]], <8 x float> [[X]], <8 x float> splat (float 1.000000e+00)
-; CHECK-NEXT:    [[ORDERED:%.*]] = fcmp ord <8 x float> [[X]], zeroinitializer
-; CHECK-NEXT:    [[LOWER_OR_FALLBACK:%.*]] = select nsz <8 x i1> [[ORDERED]], <8 x float> zeroinitializer, <8 x float> [[FALLBACK]]
-; CHECK-NEXT:    [[CLAMP_OR_FALLBACK:%.*]] = select nsz <8 x i1> [[BELOW_LO_OR_NAN]], <8 x float> [[LOWER_OR_FALLBACK]], <8 x float> [[UPPER]]
+; CHECK-NEXT:    [[ISNAN:%.*]] = fcmp uno <8 x float> [[X]], zeroinitializer
+; CHECK-NEXT:    [[TMP1:%.*]] = call nsz <8 x float> @llvm.minnum.v8f32(<8 x float> [[X]], <8 x float> splat (float 1.000000e+00))
+; CHECK-NEXT:    [[TMP2:%.*]] = call nsz <8 x float> @llvm.maxnum.v8f32(<8 x float> [[TMP1]], <8 x float> zeroinitializer)
+; CHECK-NEXT:    [[CLAMP_OR_FALLBACK:%.*]] = select <8 x i1> [[ISNAN]], <8 x float> [[FALLBACK]], <8 x float> [[TMP2]]
 ; CHECK-NEXT:    ret <8 x float> [[CLAMP_OR_FALLBACK]]
 ;
   %below_lo_or_nan = fcmp nsz ult <8 x float> %x, splat (float 0.0)
@@ -50,12 +46,10 @@ define <8 x float> @pos_vector(<8 x float> %x, <8 x float> %fallback) {
 define float @pos_no_fcmp_nsz(float %x, float %fallback) {
 ; CHECK-LABEL: define float @pos_no_fcmp_nsz(
 ; CHECK-SAME: float [[X:%.*]], float [[FALLBACK:%.*]]) {
-; CHECK-NEXT:    [[BELOW_LO_OR_NAN:%.*]] = fcmp ult float [[X]], 0.000000e+00
-; CHECK-NEXT:    [[BELOW_HI:%.*]] = fcmp olt float [[X]], 1.000000e+00
-; CHECK-NEXT:    [[UPPER:%.*]] = select nsz i1 [[BELOW_HI]], float [[X]], float 1.000000e+00
-; CHECK-NEXT:    [[ORDERED:%.*]] = fcmp ord float [[X]], 0.000000e+00
-; CHECK-NEXT:    [[LOWER_OR_FALLBACK:%.*]] = select nsz i1 [[ORDERED]], float 0.000000e+00, float [[FALLBACK]]
-; CHECK-NEXT:    [[RESULT:%.*]] = select nsz i1 [[BELOW_LO_OR_NAN]], float [[LOWER_OR_FALLBACK]], float [[UPPER]]
+; CHECK-NEXT:    [[ISNAN:%.*]] = fcmp uno float [[X]], 0.000000e+00
+; CHECK-NEXT:    [[TMP1:%.*]] = call nsz float @llvm.minnum.f32(float [[X]], float 1.000000e+00)
+; CHECK-NEXT:    [[TMP2:%.*]] = call nsz float @llvm.maxnum.f32(float [[TMP1]], float 0.000000e+00)
+; CHECK-NEXT:    [[RESULT:%.*]] = select i1 [[ISNAN]], float [[FALLBACK]], float [[TMP2]]
 ; CHECK-NEXT:    ret float [[RESULT]]
 ;
   %below_lo_or_nan = fcmp ult float %x, 0.0



More information about the llvm-commits mailing list