[llvm] [InstCombine] Fold fract(x) == 0 idiom to trunc(x) == x (PR #221523)
Jun Yeong Kim via llvm-commits
llvm-commits at lists.llvm.org
Sat Sep 5 22:24:30 PDT 2026
https://github.com/junyeong0619 created https://github.com/llvm/llvm-project/pull/221523
Fixes #188035.
Fold the inlined `fract(x) == 0` integral test to `fcmp oeq trunc(x), x`. When
a `fract()`-style "fractional part is zero" check is inlined, it expands into a
`floor`/`fsub`/`select`/`fabs` tree guarded for NaN and Inf:
```llvm
%fl = call float @llvm.floor.f32(float %x)
%sub = fsub nsz float %x, %fl
%clamp = call nsz float @llvm.minnum.f32(float %sub, float 0x3FEFFFFFE0000000)
%isnan = fcmp uno float %x, 0.0
%cond = select i1 %isnan, float %x, float %clamp
%fa = call float @llvm.fabs.f32(float %x)
%isinf = fcmp oeq float %fa, +inf
%fraczero = fcmp oeq float %cond, 0.0
%r = or i1 %isinf, %fraczero
; =>
%t = call float @llvm.trunc.f32(float %x)
%r = fcmp oeq float %t, %x
```
The `isnan` select and the `or isinf` term are load-bearing: NaN must compare
false, and infinities (where `x - floor(x)` is NaN) are covered separately. The
`minnum` clamp only keeps a real `fract()` result in `[0, 1)`, so it is matched
**optionally** (it does not affect the `== 0` test). All intermediate nodes must
be one-use so the whole tree is removed. The fold works on scalar, fixed and
scalable vectors.
### Correctness
The transformation is exact for every input (no fast-math flags required or
produced). The original end-to-end proof from the issue is
https://alive2.llvm.org/ce/z/-Ms4-r (the full single query is slow and may time
out). It also verifies in composable steps that stay well within Alive2's
budget:
- `fcmp oeq (select (uno x, 0), x, v), 0` ≡ `!isnan(x) & (v == 0)`
- `fcmp oeq minnum(a, C), 0` ≡ `fcmp oeq a, 0` for a positive constant `C`
- `or(isinf x, (ord x) & (x - floor(x) == 0))` ≡ `fcmp oeq floor(x), x`
- `fcmp oeq floor(x), x` ≡ `fcmp oeq trunc(x), x` (already in tree, #190620)
As an independent check, the whole tree and `trunc(x) == x` were compared
exhaustively over all `2^32` `float` values with 0 mismatches.
### Note
@arsenm has already changed the ROCm device-libs source to avoid emitting this
pattern, so this is now a general canonicalization rather than a fix for that
specific code. Matching the `minnum` clamp optionally keeps it applicable to
both variants of the idiom.
---
This patch was developed with the assistance of an AI tool (per the LLVM AI Tool
Policy).
>From 86443e7562a9028067235343a198f55a4662ce5b Mon Sep 17 00:00:00 2001
From: Jun Yeong Kim <junyeonggim5 at gmail.com>
Date: Sun, 6 Sep 2026 00:26:12 +0900
Subject: [PATCH 1/2] [InstCombine] Add baseline tests for fract(x) == 0 idiom
Pre-commit tests for issue #188035; the fract(x) == 0 tree is not folded yet.
---
.../Transforms/InstCombine/fract-is-zero.ll | 295 ++++++++++++++++++
1 file changed, 295 insertions(+)
create mode 100644 llvm/test/Transforms/InstCombine/fract-is-zero.ll
diff --git a/llvm/test/Transforms/InstCombine/fract-is-zero.ll b/llvm/test/Transforms/InstCombine/fract-is-zero.ll
new file mode 100644
index 0000000000000..f94d579c3bdfe
--- /dev/null
+++ b/llvm/test/Transforms/InstCombine/fract-is-zero.ll
@@ -0,0 +1,295 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; RUN: opt < %s -passes=instcombine -S | FileCheck %s
+
+; Fold the inlined "fract(x) == 0" integral test to `fcmp oeq trunc(x), x`.
+; See https://github.com/llvm/llvm-project/issues/188035.
+
+declare float @llvm.floor.f32(float)
+declare float @llvm.minnum.f32(float, float)
+declare float @llvm.fabs.f32(float)
+declare double @llvm.floor.f64(double)
+declare double @llvm.minnum.f64(double, double)
+declare double @llvm.fabs.f64(double)
+declare <2 x float> @llvm.floor.v2f32(<2 x float>)
+declare <2 x float> @llvm.minnum.v2f32(<2 x float>, <2 x float>)
+declare <2 x float> @llvm.fabs.v2f32(<2 x float>)
+declare <vscale x 2 x float> @llvm.floor.nxv2f32(<vscale x 2 x float>)
+declare <vscale x 2 x float> @llvm.minnum.nxv2f32(<vscale x 2 x float>, <vscale x 2 x float>)
+declare <vscale x 2 x float> @llvm.fabs.nxv2f32(<vscale x 2 x float>)
+declare void @use.f32(float)
+
+; Full idiom with the minnum clamp.
+define i1 @pos_with_clamp(float %x) {
+; CHECK-LABEL: define i1 @pos_with_clamp(
+; CHECK-SAME: float [[X:%.*]]) {
+; CHECK-NEXT: [[FL:%.*]] = call float @llvm.floor.f32(float [[X]])
+; CHECK-NEXT: [[SUB:%.*]] = fsub nsz float [[X]], [[FL]]
+; CHECK-NEXT: [[CLAMP:%.*]] = call nsz float @llvm.minnum.f32(float [[SUB]], float f0x3F7FFFFF)
+; CHECK-NEXT: [[ISNAN:%.*]] = fcmp uno float [[X]], 0.000000e+00
+; CHECK-NEXT: [[COND:%.*]] = select i1 [[ISNAN]], float [[X]], float [[CLAMP]]
+; CHECK-NEXT: [[FA:%.*]] = call float @llvm.fabs.f32(float [[X]])
+; CHECK-NEXT: [[ISINF:%.*]] = fcmp oeq float [[FA]], +inf
+; CHECK-NEXT: [[FRACZERO:%.*]] = fcmp oeq float [[COND]], 0.000000e+00
+; CHECK-NEXT: [[R:%.*]] = or i1 [[ISINF]], [[FRACZERO]]
+; CHECK-NEXT: ret i1 [[R]]
+;
+ %fl = call float @llvm.floor.f32(float %x)
+ %sub = fsub nsz float %x, %fl
+ %clamp = call nsz float @llvm.minnum.f32(float %sub, float 0x3FEFFFFFE0000000)
+ %isnan = fcmp uno float %x, 0.0
+ %cond = select i1 %isnan, float %x, float %clamp
+ %fa = call float @llvm.fabs.f32(float %x)
+ %isinf = fcmp oeq float %fa, +inf
+ %fraczero = fcmp oeq float %cond, 0.0
+ %r = or i1 %isinf, %fraczero
+ ret i1 %r
+}
+
+; Same idiom without the (optional) minnum clamp.
+define i1 @pos_no_clamp(float %x) {
+; CHECK-LABEL: define i1 @pos_no_clamp(
+; CHECK-SAME: float [[X:%.*]]) {
+; CHECK-NEXT: [[FL:%.*]] = call float @llvm.floor.f32(float [[X]])
+; CHECK-NEXT: [[SUB:%.*]] = fsub nsz float [[X]], [[FL]]
+; CHECK-NEXT: [[ISNAN:%.*]] = fcmp uno float [[X]], 0.000000e+00
+; CHECK-NEXT: [[COND:%.*]] = select i1 [[ISNAN]], float [[X]], float [[SUB]]
+; CHECK-NEXT: [[FA:%.*]] = call float @llvm.fabs.f32(float [[X]])
+; CHECK-NEXT: [[ISINF:%.*]] = fcmp oeq float [[FA]], +inf
+; CHECK-NEXT: [[FRACZERO:%.*]] = fcmp oeq float [[COND]], 0.000000e+00
+; CHECK-NEXT: [[R:%.*]] = or i1 [[ISINF]], [[FRACZERO]]
+; CHECK-NEXT: ret i1 [[R]]
+;
+ %fl = call float @llvm.floor.f32(float %x)
+ %sub = fsub nsz float %x, %fl
+ %isnan = fcmp uno float %x, 0.0
+ %cond = select i1 %isnan, float %x, float %sub
+ %fa = call float @llvm.fabs.f32(float %x)
+ %isinf = fcmp oeq float %fa, +inf
+ %fraczero = fcmp oeq float %cond, 0.0
+ %r = or i1 %isinf, %fraczero
+ ret i1 %r
+}
+
+; Operands of the `or` commuted.
+define i1 @pos_or_commuted(float %x) {
+; CHECK-LABEL: define i1 @pos_or_commuted(
+; CHECK-SAME: float [[X:%.*]]) {
+; CHECK-NEXT: [[FL:%.*]] = call float @llvm.floor.f32(float [[X]])
+; CHECK-NEXT: [[SUB:%.*]] = fsub nsz float [[X]], [[FL]]
+; CHECK-NEXT: [[CLAMP:%.*]] = call nsz float @llvm.minnum.f32(float [[SUB]], float f0x3F7FFFFF)
+; CHECK-NEXT: [[ISNAN:%.*]] = fcmp uno float [[X]], 0.000000e+00
+; CHECK-NEXT: [[COND:%.*]] = select i1 [[ISNAN]], float [[X]], float [[CLAMP]]
+; CHECK-NEXT: [[FA:%.*]] = call float @llvm.fabs.f32(float [[X]])
+; CHECK-NEXT: [[ISINF:%.*]] = fcmp oeq float [[FA]], +inf
+; CHECK-NEXT: [[FRACZERO:%.*]] = fcmp oeq float [[COND]], 0.000000e+00
+; CHECK-NEXT: [[R:%.*]] = or i1 [[FRACZERO]], [[ISINF]]
+; CHECK-NEXT: ret i1 [[R]]
+;
+ %fl = call float @llvm.floor.f32(float %x)
+ %sub = fsub nsz float %x, %fl
+ %clamp = call nsz float @llvm.minnum.f32(float %sub, float 0x3FEFFFFFE0000000)
+ %isnan = fcmp uno float %x, 0.0
+ %cond = select i1 %isnan, float %x, float %clamp
+ %fa = call float @llvm.fabs.f32(float %x)
+ %isinf = fcmp oeq float %fa, +inf
+ %fraczero = fcmp oeq float %cond, 0.0
+ %r = or i1 %fraczero, %isinf
+ ret i1 %r
+}
+
+define <2 x i1> @pos_vector(<2 x float> %x) {
+; CHECK-LABEL: define <2 x i1> @pos_vector(
+; CHECK-SAME: <2 x float> [[X:%.*]]) {
+; CHECK-NEXT: [[FL:%.*]] = call <2 x float> @llvm.floor.v2f32(<2 x float> [[X]])
+; CHECK-NEXT: [[SUB:%.*]] = fsub nsz <2 x float> [[X]], [[FL]]
+; CHECK-NEXT: [[CLAMP:%.*]] = call nsz <2 x float> @llvm.minnum.v2f32(<2 x float> [[SUB]], <2 x float> splat (float f0x3F7FFFFF))
+; CHECK-NEXT: [[ISNAN:%.*]] = fcmp uno <2 x float> [[X]], zeroinitializer
+; CHECK-NEXT: [[COND:%.*]] = select <2 x i1> [[ISNAN]], <2 x float> [[X]], <2 x float> [[CLAMP]]
+; CHECK-NEXT: [[FA:%.*]] = call <2 x float> @llvm.fabs.v2f32(<2 x float> [[X]])
+; CHECK-NEXT: [[ISINF:%.*]] = fcmp oeq <2 x float> [[FA]], splat (float +inf)
+; CHECK-NEXT: [[FRACZERO:%.*]] = fcmp oeq <2 x float> [[COND]], zeroinitializer
+; CHECK-NEXT: [[R:%.*]] = or <2 x i1> [[ISINF]], [[FRACZERO]]
+; CHECK-NEXT: ret <2 x i1> [[R]]
+;
+ %fl = call <2 x float> @llvm.floor.v2f32(<2 x float> %x)
+ %sub = fsub nsz <2 x float> %x, %fl
+ %clamp = call nsz <2 x float> @llvm.minnum.v2f32(<2 x float> %sub, <2 x float> splat(float 0x3FEFFFFFE0000000))
+ %isnan = fcmp uno <2 x float> %x, zeroinitializer
+ %cond = select <2 x i1> %isnan, <2 x float> %x, <2 x float> %clamp
+ %fa = call <2 x float> @llvm.fabs.v2f32(<2 x float> %x)
+ %isinf = fcmp oeq <2 x float> %fa, splat(float +inf)
+ %fraczero = fcmp oeq <2 x float> %cond, zeroinitializer
+ %r = or <2 x i1> %isinf, %fraczero
+ ret <2 x i1> %r
+}
+
+define <vscale x 2 x i1> @pos_scalable(<vscale x 2 x float> %x) {
+; CHECK-LABEL: define <vscale x 2 x i1> @pos_scalable(
+; CHECK-SAME: <vscale x 2 x float> [[X:%.*]]) {
+; CHECK-NEXT: [[FL:%.*]] = call <vscale x 2 x float> @llvm.floor.nxv2f32(<vscale x 2 x float> [[X]])
+; CHECK-NEXT: [[SUB:%.*]] = fsub nsz <vscale x 2 x float> [[X]], [[FL]]
+; CHECK-NEXT: [[CLAMP:%.*]] = call nsz <vscale x 2 x float> @llvm.minnum.nxv2f32(<vscale x 2 x float> [[SUB]], <vscale x 2 x float> splat (float f0x3F7FFFFF))
+; CHECK-NEXT: [[ISNAN:%.*]] = fcmp uno <vscale x 2 x float> [[X]], zeroinitializer
+; CHECK-NEXT: [[COND:%.*]] = select <vscale x 2 x i1> [[ISNAN]], <vscale x 2 x float> [[X]], <vscale x 2 x float> [[CLAMP]]
+; CHECK-NEXT: [[FA:%.*]] = call <vscale x 2 x float> @llvm.fabs.nxv2f32(<vscale x 2 x float> [[X]])
+; CHECK-NEXT: [[ISINF:%.*]] = fcmp oeq <vscale x 2 x float> [[FA]], splat (float +inf)
+; CHECK-NEXT: [[FRACZERO:%.*]] = fcmp oeq <vscale x 2 x float> [[COND]], zeroinitializer
+; CHECK-NEXT: [[R:%.*]] = or <vscale x 2 x i1> [[ISINF]], [[FRACZERO]]
+; CHECK-NEXT: ret <vscale x 2 x i1> [[R]]
+;
+ %fl = call <vscale x 2 x float> @llvm.floor.nxv2f32(<vscale x 2 x float> %x)
+ %sub = fsub nsz <vscale x 2 x float> %x, %fl
+ %clamp = call nsz <vscale x 2 x float> @llvm.minnum.nxv2f32(<vscale x 2 x float> %sub, <vscale x 2 x float> splat(float 0x3FEFFFFFE0000000))
+ %isnan = fcmp uno <vscale x 2 x float> %x, zeroinitializer
+ %cond = select <vscale x 2 x i1> %isnan, <vscale x 2 x float> %x, <vscale x 2 x float> %clamp
+ %fa = call <vscale x 2 x float> @llvm.fabs.nxv2f32(<vscale x 2 x float> %x)
+ %isinf = fcmp oeq <vscale x 2 x float> %fa, splat(float +inf)
+ %fraczero = fcmp oeq <vscale x 2 x float> %cond, zeroinitializer
+ %r = or <vscale x 2 x i1> %isinf, %fraczero
+ ret <vscale x 2 x i1> %r
+}
+
+define i1 @pos_double(double %x) {
+; CHECK-LABEL: define i1 @pos_double(
+; CHECK-SAME: double [[X:%.*]]) {
+; CHECK-NEXT: [[FL:%.*]] = call double @llvm.floor.f64(double [[X]])
+; CHECK-NEXT: [[SUB:%.*]] = fsub nsz double [[X]], [[FL]]
+; CHECK-NEXT: [[CLAMP:%.*]] = call nsz double @llvm.minnum.f64(double [[SUB]], double f0x3FEFFFFFFFFFFFFF)
+; CHECK-NEXT: [[ISNAN:%.*]] = fcmp uno double [[X]], 0.000000e+00
+; CHECK-NEXT: [[COND:%.*]] = select i1 [[ISNAN]], double [[X]], double [[CLAMP]]
+; CHECK-NEXT: [[FA:%.*]] = call double @llvm.fabs.f64(double [[X]])
+; CHECK-NEXT: [[ISINF:%.*]] = fcmp oeq double [[FA]], +inf
+; CHECK-NEXT: [[FRACZERO:%.*]] = fcmp oeq double [[COND]], 0.000000e+00
+; CHECK-NEXT: [[R:%.*]] = or i1 [[ISINF]], [[FRACZERO]]
+; CHECK-NEXT: ret i1 [[R]]
+;
+ %fl = call double @llvm.floor.f64(double %x)
+ %sub = fsub nsz double %x, %fl
+ %clamp = call nsz double @llvm.minnum.f64(double %sub, double 0x3FEFFFFFFFFFFFFF)
+ %isnan = fcmp uno double %x, 0.0
+ %cond = select i1 %isnan, double %x, double %clamp
+ %fa = call double @llvm.fabs.f64(double %x)
+ %isinf = fcmp oeq double %fa, +inf
+ %fraczero = fcmp oeq double %cond, 0.0
+ %r = or i1 %isinf, %fraczero
+ ret i1 %r
+}
+
+; Negative: minnum clamp constant is negative (minnum(a, C) == 0 is not a == 0).
+define i1 @neg_clamp_negative(float %x) {
+; CHECK-LABEL: define i1 @neg_clamp_negative(
+; CHECK-SAME: float [[X:%.*]]) {
+; CHECK-NEXT: [[FA:%.*]] = call float @llvm.fabs.f32(float [[X]])
+; CHECK-NEXT: [[ISINF:%.*]] = fcmp oeq float [[FA]], +inf
+; CHECK-NEXT: ret i1 [[ISINF]]
+;
+ %fl = call float @llvm.floor.f32(float %x)
+ %sub = fsub nsz float %x, %fl
+ %clamp = call nsz float @llvm.minnum.f32(float %sub, float -1.0)
+ %isnan = fcmp uno float %x, 0.0
+ %cond = select i1 %isnan, float %x, float %clamp
+ %fa = call float @llvm.fabs.f32(float %x)
+ %isinf = fcmp oeq float %fa, +inf
+ %fraczero = fcmp oeq float %cond, 0.0
+ %r = or i1 %isinf, %fraczero
+ ret i1 %r
+}
+
+; Negative: the second `or` operand is not the isinf test.
+define i1 @neg_no_isinf(float %x, i1 %other) {
+; CHECK-LABEL: define i1 @neg_no_isinf(
+; CHECK-SAME: float [[X:%.*]], i1 [[OTHER:%.*]]) {
+; CHECK-NEXT: [[FL:%.*]] = call float @llvm.floor.f32(float [[X]])
+; CHECK-NEXT: [[SUB:%.*]] = fsub nsz float [[X]], [[FL]]
+; CHECK-NEXT: [[CLAMP:%.*]] = call nsz float @llvm.minnum.f32(float [[SUB]], float f0x3F7FFFFF)
+; CHECK-NEXT: [[ISNAN:%.*]] = fcmp uno float [[X]], 0.000000e+00
+; CHECK-NEXT: [[COND:%.*]] = select i1 [[ISNAN]], float [[X]], float [[CLAMP]]
+; CHECK-NEXT: [[FRACZERO:%.*]] = fcmp oeq float [[COND]], 0.000000e+00
+; CHECK-NEXT: [[R:%.*]] = or i1 [[OTHER]], [[FRACZERO]]
+; CHECK-NEXT: ret i1 [[R]]
+;
+ %fl = call float @llvm.floor.f32(float %x)
+ %sub = fsub nsz float %x, %fl
+ %clamp = call nsz float @llvm.minnum.f32(float %sub, float 0x3FEFFFFFE0000000)
+ %isnan = fcmp uno float %x, 0.0
+ %cond = select i1 %isnan, float %x, float %clamp
+ %fraczero = fcmp oeq float %cond, 0.0
+ %r = or i1 %other, %fraczero
+ ret i1 %r
+}
+
+; Negative: compares the fractional part to 1.0 rather than 0.0.
+define i1 @neg_cmp_one(float %x) {
+; CHECK-LABEL: define i1 @neg_cmp_one(
+; CHECK-SAME: float [[X:%.*]]) {
+; CHECK-NEXT: [[FA:%.*]] = call float @llvm.fabs.f32(float [[X]])
+; CHECK-NEXT: [[ISINF:%.*]] = fcmp oeq float [[FA]], +inf
+; CHECK-NEXT: ret i1 [[ISINF]]
+;
+ %fl = call float @llvm.floor.f32(float %x)
+ %sub = fsub nsz float %x, %fl
+ %clamp = call nsz float @llvm.minnum.f32(float %sub, float 0x3FEFFFFFE0000000)
+ %isnan = fcmp uno float %x, 0.0
+ %cond = select i1 %isnan, float %x, float %clamp
+ %fa = call float @llvm.fabs.f32(float %x)
+ %isinf = fcmp oeq float %fa, +inf
+ %fracone = fcmp oeq float %cond, 1.0
+ %r = or i1 %isinf, %fracone
+ ret i1 %r
+}
+
+; Negative: an intermediate value has an extra use, so the tree is not removed.
+define i1 @neg_multi_use(float %x, ptr %p) {
+; CHECK-LABEL: define i1 @neg_multi_use(
+; CHECK-SAME: float [[X:%.*]], ptr [[P:%.*]]) {
+; CHECK-NEXT: [[FL:%.*]] = call float @llvm.floor.f32(float [[X]])
+; CHECK-NEXT: [[SUB:%.*]] = fsub nsz float [[X]], [[FL]]
+; CHECK-NEXT: [[CLAMP:%.*]] = call nsz float @llvm.minnum.f32(float [[SUB]], float f0x3F7FFFFF)
+; CHECK-NEXT: [[ISNAN:%.*]] = fcmp uno float [[X]], 0.000000e+00
+; CHECK-NEXT: [[COND:%.*]] = select i1 [[ISNAN]], float [[X]], float [[CLAMP]]
+; CHECK-NEXT: store float [[COND]], ptr [[P]], align 4
+; CHECK-NEXT: [[FA:%.*]] = call float @llvm.fabs.f32(float [[X]])
+; CHECK-NEXT: [[ISINF:%.*]] = fcmp oeq float [[FA]], +inf
+; CHECK-NEXT: [[FRACZERO:%.*]] = fcmp oeq float [[COND]], 0.000000e+00
+; CHECK-NEXT: [[R:%.*]] = or i1 [[ISINF]], [[FRACZERO]]
+; CHECK-NEXT: ret i1 [[R]]
+;
+ %fl = call float @llvm.floor.f32(float %x)
+ %sub = fsub nsz float %x, %fl
+ %clamp = call nsz float @llvm.minnum.f32(float %sub, float 0x3FEFFFFFE0000000)
+ %isnan = fcmp uno float %x, 0.0
+ %cond = select i1 %isnan, float %x, float %clamp
+ store float %cond, ptr %p
+ %fa = call float @llvm.fabs.f32(float %x)
+ %isinf = fcmp oeq float %fa, +inf
+ %fraczero = fcmp oeq float %cond, 0.0
+ %r = or i1 %isinf, %fraczero
+ ret i1 %r
+}
+
+; Negative: the select true value is not the compared input x.
+define i1 @neg_wrong_true_val(float %x, float %y) {
+; CHECK-LABEL: define i1 @neg_wrong_true_val(
+; CHECK-SAME: float [[X:%.*]], float [[Y:%.*]]) {
+; CHECK-NEXT: [[FL:%.*]] = call float @llvm.floor.f32(float [[X]])
+; CHECK-NEXT: [[SUB:%.*]] = fsub nsz float [[X]], [[FL]]
+; CHECK-NEXT: [[CLAMP:%.*]] = call nsz float @llvm.minnum.f32(float [[SUB]], float f0x3F7FFFFF)
+; CHECK-NEXT: [[ISNAN:%.*]] = fcmp uno float [[X]], 0.000000e+00
+; CHECK-NEXT: [[COND:%.*]] = select i1 [[ISNAN]], float [[Y]], float [[CLAMP]]
+; CHECK-NEXT: [[FA:%.*]] = call float @llvm.fabs.f32(float [[X]])
+; CHECK-NEXT: [[ISINF:%.*]] = fcmp oeq float [[FA]], +inf
+; CHECK-NEXT: [[FRACZERO:%.*]] = fcmp oeq float [[COND]], 0.000000e+00
+; CHECK-NEXT: [[R:%.*]] = or i1 [[ISINF]], [[FRACZERO]]
+; CHECK-NEXT: ret i1 [[R]]
+;
+ %fl = call float @llvm.floor.f32(float %x)
+ %sub = fsub nsz float %x, %fl
+ %clamp = call nsz float @llvm.minnum.f32(float %sub, float 0x3FEFFFFFE0000000)
+ %isnan = fcmp uno float %x, 0.0
+ %cond = select i1 %isnan, float %y, float %clamp
+ %fa = call float @llvm.fabs.f32(float %x)
+ %isinf = fcmp oeq float %fa, +inf
+ %fraczero = fcmp oeq float %cond, 0.0
+ %r = or i1 %isinf, %fraczero
+ ret i1 %r
+}
>From c047a38efc67db858773d398fb8e25237b19d09d Mon Sep 17 00:00:00 2001
From: Jun Yeong Kim <junyeonggim5 at gmail.com>
Date: Sun, 6 Sep 2026 00:27:23 +0900
Subject: [PATCH 2/2] [InstCombine] Fold fract(x) == 0 idiom to trunc(x) == x
Recognize the inlined "fractional part is zero" tree (floor/fsub/select/fabs
guards) and rewrite it to fcmp oeq trunc(x), x. The minnum clamp that keeps a
real fract() in [0, 1) is matched optionally, since it does not affect the
== 0 test. Works on scalar, fixed and scalable vectors.
Fixes #188035
---
.../InstCombine/InstCombineAndOrXor.cpp | 83 +++++++++++++++++++
.../Transforms/InstCombine/fract-is-zero.ll | 65 +++------------
2 files changed, 95 insertions(+), 53 deletions(-)
diff --git a/llvm/lib/Transforms/InstCombine/InstCombineAndOrXor.cpp b/llvm/lib/Transforms/InstCombine/InstCombineAndOrXor.cpp
index 9e9ebfe5c5e90..52973817aaa4e 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineAndOrXor.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineAndOrXor.cpp
@@ -4137,6 +4137,86 @@ Instruction *InstCombinerImpl::FoldOrOfLogicalAnds(Value *Op0, Value *Op1) {
// FIXME: We use commutative matchers (m_c_*) for some, but not all, matches
// here. We should standardize that construct where it is needed or choose some
// other way to ensure that commutated variants of patterns are not missed.
+/// Fold the inlined `fract(x) == 0` integral test to `fcmp oeq trunc(x), x`.
+///
+/// The tree produced by inlining a `fract()`:
+///
+/// isinf = fcmp oeq (fabs x), +inf
+/// isnan = fcmp uno x, 0.0
+/// cond = select isnan, x, minnum(fsub x, floor(x), C)
+/// fraczero = fcmp oeq cond, 0.0
+/// result = or isinf, fraczero
+///
+/// The `isnan` select and the `or isinf` term are load-bearing: NaN must
+/// compare false, and infinities (where `x - floor(x)` is NaN) are covered
+/// separately. The `minnum` clamp keeps a real fract() in [0, 1) and is
+/// matched optionally, since it does not affect the `== 0` test. All
+/// intermediate nodes must be one-use so the whole tree is removed.
+static Instruction *foldFractIsZeroToTrunc(BinaryOperator &Or,
+ InstCombinerImpl &IC) {
+ if (!Or.getType()->isIntOrIntVectorTy(1))
+ return nullptr;
+
+ Value *X = nullptr;
+
+ // frac == 0 side:
+ // fcmp oeq (select (fcmp uno x, 0), x, [minnum(]fsub x, floor(x)[, C)]), 0
+ auto MatchFracZero = [&](Value *V) -> bool {
+ Value *Sel;
+ if (!match(V, m_OneUse(m_SpecificFCmp(FCmpInst::FCMP_OEQ, m_Value(Sel),
+ m_AnyZeroFP()))))
+ return false;
+ Value *Cond, *TrueV, *FalseV;
+ if (!match(Sel, m_OneUse(m_Select(m_Value(Cond), m_Value(TrueV),
+ m_Value(FalseV)))))
+ return false;
+ Value *Xc;
+ if (!match(Cond, m_OneUse(m_SpecificFCmp(FCmpInst::FCMP_UNO, m_Value(Xc),
+ m_AnyZeroFP()))) ||
+ TrueV != Xc)
+ return false;
+ // Optional minnum(frac, C) clamp with a positive constant. It only bounds
+ // the value away from 1.0, so it is irrelevant to `frac == 0`; accept the
+ // tree with or without it. (minnum(a, C) == 0 iff a == 0 when C > 0.)
+ Value *Frac = FalseV;
+ const APFloat *C;
+ Value *ClampIn;
+ if (match(Frac, m_OneUse(m_Intrinsic<Intrinsic::minnum>(m_Value(ClampIn),
+ m_APFloat(C))))) {
+ if (C->isNaN() || C->isNegative() || C->isZero())
+ return false;
+ Frac = ClampIn;
+ }
+ // frac = x - floor(x)
+ Value *Floor;
+ if (!match(Frac, m_OneUse(m_FSub(m_Specific(Xc), m_Value(Floor)))) ||
+ !match(Floor, m_OneUse(m_Intrinsic<Intrinsic::floor>(m_Specific(Xc)))))
+ return false;
+ X = Xc;
+ return true;
+ };
+
+ // isinf side: fcmp oeq (fabs x), +inf
+ auto MatchIsInf = [&](Value *V) -> bool {
+ const APFloat *C;
+ Value *Fabs;
+ if (!match(V, m_OneUse(m_SpecificFCmp(
+ FCmpInst::FCMP_OEQ,
+ m_OneUse(m_Intrinsic<Intrinsic::fabs>(m_Value(Fabs))),
+ m_APFloat(C)))))
+ return false;
+ return C->isInfinity() && !C->isNegative() && Fabs == X;
+ };
+
+ Value *Op0 = Or.getOperand(0), *Op1 = Or.getOperand(1);
+ if (!((MatchFracZero(Op0) && MatchIsInf(Op1)) ||
+ (MatchFracZero(Op1) && MatchIsInf(Op0))))
+ return nullptr;
+
+ Value *Trunc = IC.Builder.CreateUnaryIntrinsic(Intrinsic::trunc, X);
+ return new FCmpInst(FCmpInst::FCMP_OEQ, Trunc, X);
+}
+
Instruction *InstCombinerImpl::visitOr(BinaryOperator &I) {
if (Value *V = simplifyOrInst(I.getOperand(0), I.getOperand(1),
SQ.getWithInstruction(&I)))
@@ -4268,6 +4348,9 @@ Instruction *InstCombinerImpl::visitOr(BinaryOperator &I) {
}
}
+ if (Instruction *R = foldFractIsZeroToTrunc(I, *this))
+ return R;
+
// (A & C) | (B & D)
Value *A, *B, *C, *D;
if (match(Op0, m_And(m_Value(A), m_Value(C))) &&
diff --git a/llvm/test/Transforms/InstCombine/fract-is-zero.ll b/llvm/test/Transforms/InstCombine/fract-is-zero.ll
index f94d579c3bdfe..ac062fb4609cd 100644
--- a/llvm/test/Transforms/InstCombine/fract-is-zero.ll
+++ b/llvm/test/Transforms/InstCombine/fract-is-zero.ll
@@ -22,15 +22,8 @@ declare void @use.f32(float)
define i1 @pos_with_clamp(float %x) {
; CHECK-LABEL: define i1 @pos_with_clamp(
; CHECK-SAME: float [[X:%.*]]) {
-; CHECK-NEXT: [[FL:%.*]] = call float @llvm.floor.f32(float [[X]])
-; CHECK-NEXT: [[SUB:%.*]] = fsub nsz float [[X]], [[FL]]
-; CHECK-NEXT: [[CLAMP:%.*]] = call nsz float @llvm.minnum.f32(float [[SUB]], float f0x3F7FFFFF)
-; CHECK-NEXT: [[ISNAN:%.*]] = fcmp uno float [[X]], 0.000000e+00
-; CHECK-NEXT: [[COND:%.*]] = select i1 [[ISNAN]], float [[X]], float [[CLAMP]]
-; CHECK-NEXT: [[FA:%.*]] = call float @llvm.fabs.f32(float [[X]])
-; CHECK-NEXT: [[ISINF:%.*]] = fcmp oeq float [[FA]], +inf
-; CHECK-NEXT: [[FRACZERO:%.*]] = fcmp oeq float [[COND]], 0.000000e+00
-; CHECK-NEXT: [[R:%.*]] = or i1 [[ISINF]], [[FRACZERO]]
+; CHECK-NEXT: [[TMP1:%.*]] = call float @llvm.trunc.f32(float [[X]])
+; CHECK-NEXT: [[R:%.*]] = fcmp oeq float [[TMP1]], [[X]]
; CHECK-NEXT: ret i1 [[R]]
;
%fl = call float @llvm.floor.f32(float %x)
@@ -49,14 +42,8 @@ define i1 @pos_with_clamp(float %x) {
define i1 @pos_no_clamp(float %x) {
; CHECK-LABEL: define i1 @pos_no_clamp(
; CHECK-SAME: float [[X:%.*]]) {
-; CHECK-NEXT: [[FL:%.*]] = call float @llvm.floor.f32(float [[X]])
-; CHECK-NEXT: [[SUB:%.*]] = fsub nsz float [[X]], [[FL]]
-; CHECK-NEXT: [[ISNAN:%.*]] = fcmp uno float [[X]], 0.000000e+00
-; CHECK-NEXT: [[COND:%.*]] = select i1 [[ISNAN]], float [[X]], float [[SUB]]
-; CHECK-NEXT: [[FA:%.*]] = call float @llvm.fabs.f32(float [[X]])
-; CHECK-NEXT: [[ISINF:%.*]] = fcmp oeq float [[FA]], +inf
-; CHECK-NEXT: [[FRACZERO:%.*]] = fcmp oeq float [[COND]], 0.000000e+00
-; CHECK-NEXT: [[R:%.*]] = or i1 [[ISINF]], [[FRACZERO]]
+; CHECK-NEXT: [[TMP1:%.*]] = call float @llvm.trunc.f32(float [[X]])
+; CHECK-NEXT: [[R:%.*]] = fcmp oeq float [[TMP1]], [[X]]
; CHECK-NEXT: ret i1 [[R]]
;
%fl = call float @llvm.floor.f32(float %x)
@@ -74,15 +61,8 @@ define i1 @pos_no_clamp(float %x) {
define i1 @pos_or_commuted(float %x) {
; CHECK-LABEL: define i1 @pos_or_commuted(
; CHECK-SAME: float [[X:%.*]]) {
-; CHECK-NEXT: [[FL:%.*]] = call float @llvm.floor.f32(float [[X]])
-; CHECK-NEXT: [[SUB:%.*]] = fsub nsz float [[X]], [[FL]]
-; CHECK-NEXT: [[CLAMP:%.*]] = call nsz float @llvm.minnum.f32(float [[SUB]], float f0x3F7FFFFF)
-; CHECK-NEXT: [[ISNAN:%.*]] = fcmp uno float [[X]], 0.000000e+00
-; CHECK-NEXT: [[COND:%.*]] = select i1 [[ISNAN]], float [[X]], float [[CLAMP]]
-; CHECK-NEXT: [[FA:%.*]] = call float @llvm.fabs.f32(float [[X]])
-; CHECK-NEXT: [[ISINF:%.*]] = fcmp oeq float [[FA]], +inf
-; CHECK-NEXT: [[FRACZERO:%.*]] = fcmp oeq float [[COND]], 0.000000e+00
-; CHECK-NEXT: [[R:%.*]] = or i1 [[FRACZERO]], [[ISINF]]
+; CHECK-NEXT: [[TMP1:%.*]] = call float @llvm.trunc.f32(float [[X]])
+; CHECK-NEXT: [[R:%.*]] = fcmp oeq float [[TMP1]], [[X]]
; CHECK-NEXT: ret i1 [[R]]
;
%fl = call float @llvm.floor.f32(float %x)
@@ -100,15 +80,8 @@ define i1 @pos_or_commuted(float %x) {
define <2 x i1> @pos_vector(<2 x float> %x) {
; CHECK-LABEL: define <2 x i1> @pos_vector(
; CHECK-SAME: <2 x float> [[X:%.*]]) {
-; CHECK-NEXT: [[FL:%.*]] = call <2 x float> @llvm.floor.v2f32(<2 x float> [[X]])
-; CHECK-NEXT: [[SUB:%.*]] = fsub nsz <2 x float> [[X]], [[FL]]
-; CHECK-NEXT: [[CLAMP:%.*]] = call nsz <2 x float> @llvm.minnum.v2f32(<2 x float> [[SUB]], <2 x float> splat (float f0x3F7FFFFF))
-; CHECK-NEXT: [[ISNAN:%.*]] = fcmp uno <2 x float> [[X]], zeroinitializer
-; CHECK-NEXT: [[COND:%.*]] = select <2 x i1> [[ISNAN]], <2 x float> [[X]], <2 x float> [[CLAMP]]
-; CHECK-NEXT: [[FA:%.*]] = call <2 x float> @llvm.fabs.v2f32(<2 x float> [[X]])
-; CHECK-NEXT: [[ISINF:%.*]] = fcmp oeq <2 x float> [[FA]], splat (float +inf)
-; CHECK-NEXT: [[FRACZERO:%.*]] = fcmp oeq <2 x float> [[COND]], zeroinitializer
-; CHECK-NEXT: [[R:%.*]] = or <2 x i1> [[ISINF]], [[FRACZERO]]
+; CHECK-NEXT: [[TMP1:%.*]] = call <2 x float> @llvm.trunc.v2f32(<2 x float> [[X]])
+; CHECK-NEXT: [[R:%.*]] = fcmp oeq <2 x float> [[TMP1]], [[X]]
; CHECK-NEXT: ret <2 x i1> [[R]]
;
%fl = call <2 x float> @llvm.floor.v2f32(<2 x float> %x)
@@ -126,15 +99,8 @@ define <2 x i1> @pos_vector(<2 x float> %x) {
define <vscale x 2 x i1> @pos_scalable(<vscale x 2 x float> %x) {
; CHECK-LABEL: define <vscale x 2 x i1> @pos_scalable(
; CHECK-SAME: <vscale x 2 x float> [[X:%.*]]) {
-; CHECK-NEXT: [[FL:%.*]] = call <vscale x 2 x float> @llvm.floor.nxv2f32(<vscale x 2 x float> [[X]])
-; CHECK-NEXT: [[SUB:%.*]] = fsub nsz <vscale x 2 x float> [[X]], [[FL]]
-; CHECK-NEXT: [[CLAMP:%.*]] = call nsz <vscale x 2 x float> @llvm.minnum.nxv2f32(<vscale x 2 x float> [[SUB]], <vscale x 2 x float> splat (float f0x3F7FFFFF))
-; CHECK-NEXT: [[ISNAN:%.*]] = fcmp uno <vscale x 2 x float> [[X]], zeroinitializer
-; CHECK-NEXT: [[COND:%.*]] = select <vscale x 2 x i1> [[ISNAN]], <vscale x 2 x float> [[X]], <vscale x 2 x float> [[CLAMP]]
-; CHECK-NEXT: [[FA:%.*]] = call <vscale x 2 x float> @llvm.fabs.nxv2f32(<vscale x 2 x float> [[X]])
-; CHECK-NEXT: [[ISINF:%.*]] = fcmp oeq <vscale x 2 x float> [[FA]], splat (float +inf)
-; CHECK-NEXT: [[FRACZERO:%.*]] = fcmp oeq <vscale x 2 x float> [[COND]], zeroinitializer
-; CHECK-NEXT: [[R:%.*]] = or <vscale x 2 x i1> [[ISINF]], [[FRACZERO]]
+; CHECK-NEXT: [[TMP1:%.*]] = call <vscale x 2 x float> @llvm.trunc.nxv2f32(<vscale x 2 x float> [[X]])
+; CHECK-NEXT: [[R:%.*]] = fcmp oeq <vscale x 2 x float> [[TMP1]], [[X]]
; CHECK-NEXT: ret <vscale x 2 x i1> [[R]]
;
%fl = call <vscale x 2 x float> @llvm.floor.nxv2f32(<vscale x 2 x float> %x)
@@ -152,15 +118,8 @@ define <vscale x 2 x i1> @pos_scalable(<vscale x 2 x float> %x) {
define i1 @pos_double(double %x) {
; CHECK-LABEL: define i1 @pos_double(
; CHECK-SAME: double [[X:%.*]]) {
-; CHECK-NEXT: [[FL:%.*]] = call double @llvm.floor.f64(double [[X]])
-; CHECK-NEXT: [[SUB:%.*]] = fsub nsz double [[X]], [[FL]]
-; CHECK-NEXT: [[CLAMP:%.*]] = call nsz double @llvm.minnum.f64(double [[SUB]], double f0x3FEFFFFFFFFFFFFF)
-; CHECK-NEXT: [[ISNAN:%.*]] = fcmp uno double [[X]], 0.000000e+00
-; CHECK-NEXT: [[COND:%.*]] = select i1 [[ISNAN]], double [[X]], double [[CLAMP]]
-; CHECK-NEXT: [[FA:%.*]] = call double @llvm.fabs.f64(double [[X]])
-; CHECK-NEXT: [[ISINF:%.*]] = fcmp oeq double [[FA]], +inf
-; CHECK-NEXT: [[FRACZERO:%.*]] = fcmp oeq double [[COND]], 0.000000e+00
-; CHECK-NEXT: [[R:%.*]] = or i1 [[ISINF]], [[FRACZERO]]
+; CHECK-NEXT: [[TMP1:%.*]] = call double @llvm.trunc.f64(double [[X]])
+; CHECK-NEXT: [[R:%.*]] = fcmp oeq double [[TMP1]], [[X]]
; CHECK-NEXT: ret i1 [[R]]
;
%fl = call double @llvm.floor.f64(double %x)
More information about the llvm-commits
mailing list