[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