[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