[llvm] [InstCombine] Don't copy ninf when narrowing fptrunc(binop(fpext, fpext)) (PR #202489)

Justin Lebar via llvm-commits llvm-commits at lists.llvm.org
Wed Jul 1 14:52:20 PDT 2026


https://github.com/jlebar updated https://github.com/llvm/llvm-project/pull/202489

>From 8d1de5abfcdb43c6d22e3c943e8213b7053922dd Mon Sep 17 00:00:00 2001
From: Justin Lebar <justin.lebar at gmail.com>
Date: Mon, 8 Jun 2026 06:32:12 -0400
Subject: [PATCH 1/3] [InstCombine] Don't copy ninf when narrowing
 fptrunc(binop(fpext, fpext))

The fold narrows fptrunc(BO(fpext x, fpext y)) -> BO(x, y) for
fadd/fsub/fmul/fdiv, recomputing the binop directly in the narrower type.

Even if the original BO returns a finite result, the fptrunc may produce
inf. Therefore even if the original BO has ninf, we have to drop it on
the new BO.
---
 .../InstCombine/InstCombineCasts.cpp          | 12 ++-
 llvm/test/Transforms/InstCombine/fptrunc.ll   | 81 +++++++++++++++++++
 2 files changed, 90 insertions(+), 3 deletions(-)

diff --git a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
index d371218e61108..cf38d4c30ba75 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
@@ -2206,6 +2206,12 @@ Instruction *InstCombinerImpl::visitFPTrunc(FPTruncInst &FPT) {
     unsigned RHSWidth = RHSMinType->getFPMantissaWidth();
     unsigned SrcWidth = std::max(LHSWidth, RHSWidth);
     unsigned DstWidth = Ty->getFPMantissaWidth();
+
+    // Narrowing recomputes the binop in a smaller type, which can overflow to
+    // inf where the wide op was finite. Therefore we have to drop ninf.
+    FastMathFlags NarrowFMF = BO->getFastMathFlags();
+    NarrowFMF.setNoInfs(false);
+
     switch (BO->getOpcode()) {
       default: break;
       case Instruction::FAdd:
@@ -2232,7 +2238,7 @@ Instruction *InstCombinerImpl::visitFPTrunc(FPTruncInst &FPT) {
           Value *LHS = Builder.CreateFPTrunc(BO->getOperand(0), Ty);
           Value *RHS = Builder.CreateFPTrunc(BO->getOperand(1), Ty);
           Instruction *RI = BinaryOperator::Create(BO->getOpcode(), LHS, RHS);
-          RI->copyFastMathFlags(BO);
+          RI->setFastMathFlags(NarrowFMF);
           return RI;
         }
         break;
@@ -2245,7 +2251,7 @@ Instruction *InstCombinerImpl::visitFPTrunc(FPTruncInst &FPT) {
         if (OpWidth >= LHSWidth + RHSWidth && DstWidth >= SrcWidth) {
           Value *LHS = Builder.CreateFPTrunc(BO->getOperand(0), Ty);
           Value *RHS = Builder.CreateFPTrunc(BO->getOperand(1), Ty);
-          return BinaryOperator::CreateFMulFMF(LHS, RHS, BO);
+          return BinaryOperator::CreateFMulFMF(LHS, RHS, NarrowFMF);
         }
         break;
       case Instruction::FDiv:
@@ -2258,7 +2264,7 @@ Instruction *InstCombinerImpl::visitFPTrunc(FPTruncInst &FPT) {
         if (OpWidth >= 2*DstWidth && DstWidth >= SrcWidth) {
           Value *LHS = Builder.CreateFPTrunc(BO->getOperand(0), Ty);
           Value *RHS = Builder.CreateFPTrunc(BO->getOperand(1), Ty);
-          return BinaryOperator::CreateFDivFMF(LHS, RHS, BO);
+          return BinaryOperator::CreateFDivFMF(LHS, RHS, NarrowFMF);
         }
         break;
       case Instruction::FRem: {
diff --git a/llvm/test/Transforms/InstCombine/fptrunc.ll b/llvm/test/Transforms/InstCombine/fptrunc.ll
index 2601e3c60ee12..0cdfa420bc2d2 100644
--- a/llvm/test/Transforms/InstCombine/fptrunc.ll
+++ b/llvm/test/Transforms/InstCombine/fptrunc.ll
@@ -288,3 +288,84 @@ define float @fptrunc_narrow_outside_float_range(i64 %x) {
   %conv4 = fptrunc double %div3 to float
   ret float %conv4
 }
+
+; Narrowing fptrunc(binop(fpext,fpext)) recomputes the binop in a smaller type,
+; which can overflow to inf where the wide op was finite. ninf must NOT be
+; copied to the narrowed op (it would make that inf poison: miscompile). nnan is
+; value-based and stays sound, so it is preserved.
+
+define half @fmul_narrow_drop_ninf(half %x, half %y) {
+; CHECK-LABEL: @fmul_narrow_drop_ninf(
+; CHECK-NEXT:    [[R:%.*]] = fmul half [[X:%.*]], [[Y:%.*]]
+; CHECK-NEXT:    ret half [[R]]
+;
+  %wx = fpext half %x to float
+  %wy = fpext half %y to float
+  %m = fmul ninf float %wx, %wy
+  %r = fptrunc float %m to half
+  ret half %r
+}
+
+define half @fadd_narrow_drop_ninf(half %x, half %y) {
+; CHECK-LABEL: @fadd_narrow_drop_ninf(
+; CHECK-NEXT:    [[R:%.*]] = fadd half [[X:%.*]], [[Y:%.*]]
+; CHECK-NEXT:    ret half [[R]]
+;
+  %wx = fpext half %x to float
+  %wy = fpext half %y to float
+  %m = fadd ninf float %wx, %wy
+  %r = fptrunc float %m to half
+  ret half %r
+}
+
+define half @fsub_narrow_drop_ninf(half %x, half %y) {
+; CHECK-LABEL: @fsub_narrow_drop_ninf(
+; CHECK-NEXT:    [[R:%.*]] = fsub half [[X:%.*]], [[Y:%.*]]
+; CHECK-NEXT:    ret half [[R]]
+;
+  %wx = fpext half %x to float
+  %wy = fpext half %y to float
+  %m = fsub ninf float %wx, %wy
+  %r = fptrunc float %m to half
+  ret half %r
+}
+
+define half @fdiv_narrow_drop_ninf(half %x, half %y) {
+; CHECK-LABEL: @fdiv_narrow_drop_ninf(
+; CHECK-NEXT:    [[R:%.*]] = fdiv half [[X:%.*]], [[Y:%.*]]
+; CHECK-NEXT:    ret half [[R]]
+;
+  %wx = fpext half %x to float
+  %wy = fpext half %y to float
+  %m = fdiv ninf float %wx, %wy
+  %r = fptrunc float %m to half
+  ret half %r
+}
+
+; nnan is sound to keep; ninf is still dropped.
+
+define half @fmul_narrow_keep_nnan_drop_ninf(half %x, half %y) {
+; CHECK-LABEL: @fmul_narrow_keep_nnan_drop_ninf(
+; CHECK-NEXT:    [[R:%.*]] = fmul nnan half [[X:%.*]], [[Y:%.*]]
+; CHECK-NEXT:    ret half [[R]]
+;
+  %wx = fpext half %x to float
+  %wy = fpext half %y to float
+  %m = fmul nnan ninf float %wx, %wy
+  %r = fptrunc float %m to half
+  ret half %r
+}
+
+; Other flags (reassoc) are preserved; only ninf is cleared.
+
+define half @fmul_narrow_keep_reassoc_drop_ninf(half %x, half %y) {
+; CHECK-LABEL: @fmul_narrow_keep_reassoc_drop_ninf(
+; CHECK-NEXT:    [[R:%.*]] = fmul reassoc half [[X:%.*]], [[Y:%.*]]
+; CHECK-NEXT:    ret half [[R]]
+;
+  %wx = fpext half %x to float
+  %wy = fpext half %y to float
+  %m = fmul reassoc ninf float %wx, %wy
+  %r = fptrunc float %m to half
+  ret half %r
+}

>From 24d290abbfad677702d496c29de7637b859415fe Mon Sep 17 00:00:00 2001
From: Justin Lebar <jlebar at anthropic.com>
Date: Wed, 1 Jul 2026 21:21:35 +0000
Subject: [PATCH 2/3] Keep ninf only when both the binop and the fptrunc have
 it

---
 .../InstCombine/InstCombineCasts.cpp          | 10 ++-
 llvm/test/Transforms/InstCombine/fptrunc.ll   | 67 ++++++++++++++++++-
 2 files changed, 71 insertions(+), 6 deletions(-)

diff --git a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
index cf38d4c30ba75..08bb6e5b36173 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
@@ -2207,10 +2207,14 @@ Instruction *InstCombinerImpl::visitFPTrunc(FPTruncInst &FPT) {
     unsigned SrcWidth = std::max(LHSWidth, RHSWidth);
     unsigned DstWidth = Ty->getFPMantissaWidth();
 
-    // Narrowing recomputes the binop in a smaller type, which can overflow to
-    // inf where the wide op was finite. Therefore we have to drop ninf.
+    // The narrowed binop keeps ninf only if both the binop and the fptrunc
+    // have it. ninf constrains both operands and result: the binop's ninf
+    // covers the (bit-identical) narrow operands but not the result, since
+    // narrowing can overflow to inf where the wide op was finite; the
+    // fptrunc's ninf covers the result (whose value the narrowed binop
+    // takes over) but not the operands.
     FastMathFlags NarrowFMF = BO->getFastMathFlags();
-    NarrowFMF.setNoInfs(false);
+    NarrowFMF.setNoInfs(NarrowFMF.noInfs() && FPT.hasNoInfs());
 
     switch (BO->getOpcode()) {
       default: break;
diff --git a/llvm/test/Transforms/InstCombine/fptrunc.ll b/llvm/test/Transforms/InstCombine/fptrunc.ll
index 0cdfa420bc2d2..3454e7aa3f986 100644
--- a/llvm/test/Transforms/InstCombine/fptrunc.ll
+++ b/llvm/test/Transforms/InstCombine/fptrunc.ll
@@ -290,9 +290,11 @@ define float @fptrunc_narrow_outside_float_range(i64 %x) {
 }
 
 ; Narrowing fptrunc(binop(fpext,fpext)) recomputes the binop in a smaller type,
-; which can overflow to inf where the wide op was finite. ninf must NOT be
-; copied to the narrowed op (it would make that inf poison: miscompile). nnan is
-; value-based and stays sound, so it is preserved.
+; which can overflow to inf where the wide op was finite, so the binop's ninf
+; alone must NOT be copied to the narrowed op (it would make that inf poison:
+; miscompile). nnan is value-based and stays sound, so it is preserved. ninf
+; survives only when both the binop and the fptrunc have it: the binop's ninf
+; covers the narrowed op's operands, the fptrunc's covers its result.
 
 define half @fmul_narrow_drop_ninf(half %x, half %y) {
 ; CHECK-LABEL: @fmul_narrow_drop_ninf(
@@ -369,3 +371,62 @@ define half @fmul_narrow_keep_reassoc_drop_ninf(half %x, half %y) {
   %r = fptrunc float %m to half
   ret half %r
 }
+
+; ninf on the fptrunc alone must NOT transfer to the narrowed binop either:
+; ninf also constrains the operands. With x = inf, y = 0.0, the wide fmul is
+; nan and fptrunc ninf of nan is well-defined, but fmul ninf half inf, 0.0
+; would be poison.
+
+define half @fmul_narrow_ninf_only_on_fptrunc(half %x, half %y) {
+; CHECK-LABEL: @fmul_narrow_ninf_only_on_fptrunc(
+; CHECK-NEXT:    [[R:%.*]] = fmul half [[X:%.*]], [[Y:%.*]]
+; CHECK-NEXT:    ret half [[R]]
+;
+  %wx = fpext half %x to float
+  %wy = fpext half %y to float
+  %m = fmul float %wx, %wy
+  %r = fptrunc ninf float %m to half
+  ret half %r
+}
+
+; ninf on both the binop and the fptrunc is kept.
+
+define half @fmul_narrow_ninf_on_both(half %x, half %y) {
+; CHECK-LABEL: @fmul_narrow_ninf_on_both(
+; CHECK-NEXT:    [[R:%.*]] = fmul ninf half [[X:%.*]], [[Y:%.*]]
+; CHECK-NEXT:    ret half [[R]]
+;
+  %wx = fpext half %x to float
+  %wy = fpext half %y to float
+  %m = fmul ninf float %wx, %wy
+  %r = fptrunc ninf float %m to half
+  ret half %r
+}
+
+; Same for fdiv, where even a finite result shows it: with x = 1.0, y = inf,
+; the wide fdiv is 0.0 and the fptrunc ninf is fully defined, but
+; fdiv ninf half 1.0, inf would be poison.
+
+define half @fdiv_narrow_ninf_only_on_fptrunc(half %x, half %y) {
+; CHECK-LABEL: @fdiv_narrow_ninf_only_on_fptrunc(
+; CHECK-NEXT:    [[R:%.*]] = fdiv half [[X:%.*]], [[Y:%.*]]
+; CHECK-NEXT:    ret half [[R]]
+;
+  %wx = fpext half %x to float
+  %wy = fpext half %y to float
+  %m = fdiv float %wx, %wy
+  %r = fptrunc ninf float %m to half
+  ret half %r
+}
+
+define half @fdiv_narrow_ninf_on_both(half %x, half %y) {
+; CHECK-LABEL: @fdiv_narrow_ninf_on_both(
+; CHECK-NEXT:    [[R:%.*]] = fdiv ninf half [[X:%.*]], [[Y:%.*]]
+; CHECK-NEXT:    ret half [[R]]
+;
+  %wx = fpext half %x to float
+  %wy = fpext half %y to float
+  %m = fdiv ninf float %wx, %wy
+  %r = fptrunc ninf float %m to half
+  ret half %r
+}

>From eaff00196cf6e26cf2e5fe1edbd44ac313f9f894 Mon Sep 17 00:00:00 2001
From: Justin Lebar <jlebar at anthropic.com>
Date: Wed, 1 Jul 2026 21:52:09 +0000
Subject: [PATCH 3/3] Shorten comment

---
 llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp | 9 +++------
 1 file changed, 3 insertions(+), 6 deletions(-)

diff --git a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
index 08bb6e5b36173..8c0e56ede1e9b 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp
@@ -2207,12 +2207,9 @@ Instruction *InstCombinerImpl::visitFPTrunc(FPTruncInst &FPT) {
     unsigned SrcWidth = std::max(LHSWidth, RHSWidth);
     unsigned DstWidth = Ty->getFPMantissaWidth();
 
-    // The narrowed binop keeps ninf only if both the binop and the fptrunc
-    // have it. ninf constrains both operands and result: the binop's ninf
-    // covers the (bit-identical) narrow operands but not the result, since
-    // narrowing can overflow to inf where the wide op was finite; the
-    // fptrunc's ninf covers the result (whose value the narrowed binop
-    // takes over) but not the operands.
+    // Narrowing recomputes the binop in a smaller type, which can overflow to
+    // inf where the wide op was finite. Therefore we can only keep ninf if
+    // both the binop and the fptrunc have that flag.
     FastMathFlags NarrowFMF = BO->getFastMathFlags();
     NarrowFMF.setNoInfs(NarrowFMF.noInfs() && FPT.hasNoInfs());
 



More information about the llvm-commits mailing list