[Mlir-commits] [mlir] [MLIR][LLVM] Preserve constrained floating-point environment effects (PR #234506)

llvmlistbot at llvm.org llvmlistbot at llvm.org
Wed Oct 21 23:17:00 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-mlir-llvm

Author: sriramshastry

<details>
<summary>Changes</summary>

# [MLIR][LLVM] Preserve constrained floating-point environment effects

## Problem

The constrained arithmetic/conversion and comparison bases declare `Pure`, allowing optimizations
that discard observable floating-point behavior:

- Canonicalization deletes an unused `constrained.sqrt` with `strict` exception
  behavior, so evaluating a negative input no longer sets `FE_INVALID`.
- The separate `constrained.fcmp`/`constrained.fcmps` family has the same
  incorrect purity declaration, losing signaling-NaN exception behavior.
- CSE merges dynamic-rounding operations across a call that changes the rounding
  mode, changing a nonzero result to zero.

Both failures were reproduced with LLVM IR export and native x86-64 execution
at backend -O0 and -O2, with the original program as a passing control.

## Change

Replace `Pure` on both existing constrained-intrinsic bases with a shared
`MemoryEffectsOpInterface` implementation. Use conservative default-resource
reads for all constrained intrinsics and writes for exception behavior other
than `Ignore`. The operations no longer declare unconditional speculation.

Read-only ignored-exception operations remain eligible for dead-code elimination
and adjacent CSE. Environment-changing calls block CSE of dynamic-rounding
operations. This is intentionally conservative: ordinary memory writes can also
block CSE, and constrained operations are not speculated. A more precise
FP-specific resource or conditional-speculation model is not introduced here.

The 19-function regression covers strict/maytrap/ignore behavior, quiet and
signaling comparisons, conversions, vectors, operations with and without a
rounding attribute, CSE controls, a changing environment, and a possibly
zero-trip loop. Retaining unused maytrap operations is conservative, not a
claim that LLVM requires preserving every maytrap exception.

## Validation

Tested main snapshot: `1204f6b4e9d6bef8ec319d121942dca5cbf94a26`, plus this patch.
The initial build used a separate worktree and empty build directory at
`f05c112b2e455f62378de6d298b6798648a66941`, with no imported compiler objects or
libraries. For the 2026-10-22 main recheck, that isolated worktree was advanced
to the new pin, required tools rebuilt incrementally in the same build directory,
and all validation below rerun. This was not a second fresh build.
Release/assertions, X86, Clang 18.1.3, LLD, caches disabled, Python bindings
and MLIR integration tests disabled.

- New lit regression: both RUN pipelines pass.
- LLVM dialect and LLVM IR export tests: 562 passed.
- Native runtime: all 112 assertions across four pipelines and -O0/-O2 pass.
- Full configured `check-mlir`: 4,174 passed, 638 unsupported, one expected
  failure, no unexpected failures.
- Changed-line formatting, whitespace, clean-base patch application, and
  byte-for-byte comparison against the tested files pass.

The runtime checks cover invalid/valid sqrt inputs, negative zero, positive
infinity, quiet/signaling NaNs, quiet/signaling comparisons, strict SIGFPE in an
isolated child, and a guarded maytrap operation that must not introduce a trap.
Binary32 addition under downward/upward rounding has expected difference
`0x1p-23`. `Ignore` exception flags are observed but not asserted. Child processes
disable core dumps. These trap tests use Linux/glibc facilities, not portable C.

An initial audit assertion incorrectly demanded that an unused maytrap operation
must raise SIGFPE. The unoptimized control falsified that assertion. The pinned
LLVM LangRef permits maytrap transformations to hide exceptions while forbidding
new ones; the test was corrected to assert that contract instead. Comparison
FileCheck patterns were also corrected to use observed canonical syntax.

The failure baseline uses the earlier clean-built tools at `295cc9232`, whose
parent is `4c830588b1a9a4fafb892d6221167456a0344304`; it is not claimed to be
a paired build of the newer submission pin. The corrected 112-assertion baseline
has 40 mismatches across optimized runs; both unoptimized controls pass. No native
Windows, AArch64/SVE, all-target LLVM suite, or performance benchmark is claimed.
Unsupported tests are not counted as passes. Conversion/vector cases are checked
in MLIR rather than by the native runtime oracle.

## Related Work

Related: #<!-- -->205158, which proposes a broader FP-environment representation.
This patch uses only the existing constrained intrinsics and attributes on
unpatched main. It neither applies that PR nor depends on its new APIs.
It remained open at the 2026-10-22 check; the affected main definitions still
declared `Pure`. Targeted searches found no narrower landed duplicate, but are
not a guarantee that no overlapping work exists. Coordinate the overlapping
effect modelling during review.

## Disclosure

Assisted-by: GitHub Copilot


---
Full diff: https://github.com/llvm/llvm-project/pull/234506.diff


2 Files Affected:

- (modified) mlir/include/mlir/Dialect/LLVMIR/LLVMIntrinsicOps.td (+16-2) 
- (added) mlir/test/Dialect/LLVMIR/constrained-effects.mlir (+179) 


``````````diff
diff --git a/mlir/include/mlir/Dialect/LLVMIR/LLVMIntrinsicOps.td b/mlir/include/mlir/Dialect/LLVMIR/LLVMIntrinsicOps.td
index dfd36966644aa..d6f7e5850e2fb 100644
--- a/mlir/include/mlir/Dialect/LLVMIR/LLVMIntrinsicOps.td
+++ b/mlir/include/mlir/Dialect/LLVMIR/LLVMIntrinsicOps.td
@@ -452,6 +452,17 @@ def LLVM_LaunderInvariantGroupOp
 
 // Constrained Floating-Point Intrinsics.
 
+defvar constrainedFPEffects = [{
+  void getEffects(
+      ::llvm::SmallVectorImpl<::mlir::SideEffects::EffectInstance<
+          ::mlir::MemoryEffects::Effect>> &effects) {
+    effects.emplace_back(::mlir::MemoryEffects::Read::get());
+    if (getFpExceptionBehaviorAttr().getValue() !=
+        ::mlir::LLVM::FPExceptionBehavior::Ignore)
+      effects.emplace_back(::mlir::MemoryEffects::Write::get());
+  }
+}];
+
 class LLVM_ConstrainedIntr<string mnem, int numArgs,
                            bit overloadedResult, list<int> overloadedOperands,
                            bit hasRoundingMode,
@@ -461,7 +472,8 @@ class LLVM_ConstrainedIntr<string mnem, int numArgs,
                            !cond(!gt(overloadedResult, 0) : [0],
                                  true : []),
                            overloadedOperands,
-                           /*traits=*/[Pure, DeclareOpInterfaceMethods<FPExceptionBehaviorOpInterface>]
+                           /*traits=*/[MemoryEffectsOpInterface,
+                                       DeclareOpInterfaceMethods<FPExceptionBehaviorOpInterface>]
                            # !cond(
                                !gt(hasRoundingMode, 0) : [DeclareOpInterfaceMethods<RoundingModeOpInterface>],
                                true : [])
@@ -542,6 +554,7 @@ class LLVM_ConstrainedIntr<string mnem, int numArgs,
     moduleImport.setFastmathFlagsAttr(inst, op);
     $res = op;
   }];
+  let extraClassDeclaration = constrainedFPEffects;
 }
 
 // Convenience base class for constrained intrinsics that take a single
@@ -620,7 +633,7 @@ class LLVM_ConstrainedFPToIntIntrNoRM<string mnem>
 class LLVM_ConstrainedFCmpIntrBase<string mnem>
     : LLVM_OneResultIntrOp<"experimental.constrained." # mnem,
         /*overloadedResults=*/[], /*overloadedOperands=*/[0],
-        /*traits=*/[Pure, SameTypeOperands,
+        /*traits=*/[MemoryEffectsOpInterface, SameTypeOperands,
           TypesMatchWith<"result type has i1 element type and same shape as "
                          "operands", "arg_0", "res",
                          "::mlir::LLVM::getI1SameShape($_self)">,
@@ -657,6 +670,7 @@ class LLVM_ConstrainedFCmpIntrBase<string mnem>
     moduleImport.setFastmathFlagsAttr(inst, op);
     $res = op;
   }];
+  let extraClassDeclaration = constrainedFPEffects;
 }
 
 def LLVM_ConstrainedFAddIntr
diff --git a/mlir/test/Dialect/LLVMIR/constrained-effects.mlir b/mlir/test/Dialect/LLVMIR/constrained-effects.mlir
new file mode 100644
index 0000000000000..29ae9fff4ec23
--- /dev/null
+++ b/mlir/test/Dialect/LLVMIR/constrained-effects.mlir
@@ -0,0 +1,179 @@
+// RUN: mlir-opt %s -canonicalize -cse -loop-invariant-code-motion | FileCheck %s
+// RUN: mlir-opt %s -cse | FileCheck %s
+
+llvm.func @change_environment()
+
+// CHECK-LABEL: llvm.func @unused_strict
+// CHECK: llvm.intr.experimental.constrained.sqrt {{.*}} tonearest strict
+// CHECK: llvm.return
+llvm.func @unused_strict(%input: f32) {
+  %unused = llvm.intr.experimental.constrained.sqrt %input tonearest strict : f32
+  llvm.return
+}
+
+// CHECK-LABEL: llvm.func @unused_maytrap
+// CHECK: llvm.intr.experimental.constrained.sqrt {{.*}} tonearest maytrap
+// CHECK: llvm.return
+llvm.func @unused_maytrap(%input: f32) {
+  %unused = llvm.intr.experimental.constrained.sqrt %input tonearest maytrap : f32
+  llvm.return
+}
+
+// CHECK-LABEL: llvm.func @unused_ignore
+// CHECK-NOT: llvm.intr.experimental.constrained
+// CHECK: llvm.return
+llvm.func @unused_ignore(%input: f32) {
+  %unused = llvm.intr.experimental.constrained.sqrt %input tonearest ignore : f32
+  llvm.return
+}
+
+// CHECK-LABEL: llvm.func @unused_binary_strict
+// CHECK: llvm.intr.experimental.constrained.fadd {{.*}} dynamic strict
+// CHECK: llvm.return
+llvm.func @unused_binary_strict(%input: f32, %increment: f32) {
+  %unused = llvm.intr.experimental.constrained.fadd %input, %increment dynamic strict : f32
+  llvm.return
+}
+
+// CHECK-LABEL: llvm.func @unused_no_rounding_strict
+// CHECK: llvm.intr.experimental.constrained.ceil {{.*}} strict
+// CHECK: llvm.return
+llvm.func @unused_no_rounding_strict(%input: f32) {
+  %unused = llvm.intr.experimental.constrained.ceil %input strict : f32
+  llvm.return
+}
+
+// CHECK-LABEL: llvm.func @strict_not_cse
+// CHECK: llvm.intr.experimental.constrained.sqrt
+// CHECK: llvm.intr.experimental.constrained.sqrt
+// CHECK: llvm.return
+llvm.func @strict_not_cse(%input: f32) -> f32 {
+  %first = llvm.intr.experimental.constrained.sqrt %input tonearest strict : f32
+  %second = llvm.intr.experimental.constrained.sqrt %input tonearest strict : f32
+  %sum = llvm.fadd %first, %second : f32
+  llvm.return %sum : f32
+}
+
+// CHECK-LABEL: llvm.func @ignore_can_cse
+// CHECK: %[[VALUE:.*]] = llvm.intr.experimental.constrained.sqrt
+// CHECK-NOT: llvm.intr.experimental.constrained.sqrt
+// CHECK: llvm.fadd %[[VALUE]], %[[VALUE]]
+// CHECK: llvm.return
+llvm.func @ignore_can_cse(%input: f32) -> f32 {
+  %first = llvm.intr.experimental.constrained.sqrt %input tonearest ignore : f32
+  %second = llvm.intr.experimental.constrained.sqrt %input tonearest ignore : f32
+  %sum = llvm.fadd %first, %second : f32
+  llvm.return %sum : f32
+}
+
+// CHECK-LABEL: llvm.func @dynamic_rounding_not_cse
+// CHECK: llvm.intr.experimental.constrained.fadd {{.*}} dynamic ignore
+// CHECK: llvm.call @change_environment
+// CHECK: llvm.intr.experimental.constrained.fadd {{.*}} dynamic ignore
+// CHECK: llvm.return
+llvm.func @dynamic_rounding_not_cse(%input: f32, %increment: f32) -> f32 {
+  %first = llvm.intr.experimental.constrained.fadd %input, %increment dynamic ignore : f32
+  llvm.call @change_environment() : () -> ()
+  %second = llvm.intr.experimental.constrained.fadd %input, %increment dynamic ignore : f32
+  %difference = llvm.fsub %second, %first : f32
+  llvm.return %difference : f32
+}
+
+// CHECK-LABEL: func.func @strict_stays_in_loop
+// CHECK-NOT: llvm.intr.experimental.constrained.sqrt
+// CHECK: scf.for
+// CHECK: llvm.intr.experimental.constrained.sqrt {{.*}} tonearest strict
+// CHECK: return
+func.func @strict_stays_in_loop(%lower: index, %upper: index, %step: index, %input: f32) {
+  scf.for %index = %lower to %upper step %step {
+    %unused = llvm.intr.experimental.constrained.sqrt %input tonearest strict : f32
+  }
+  return
+}
+
+// CHECK-LABEL: llvm.func @unused_quiet_comparison_strict
+// CHECK: llvm.intr.experimental.constrained.fcmp oeq {{.*}} strict
+// CHECK: llvm.return
+llvm.func @unused_quiet_comparison_strict(%left: f32, %right: f32) {
+  %unused = llvm.intr.experimental.constrained.fcmp "oeq" %left, %right strict : f32
+  llvm.return
+}
+
+// CHECK-LABEL: llvm.func @unused_signaling_comparison_strict
+// CHECK: llvm.intr.experimental.constrained.fcmps oeq {{.*}} strict
+// CHECK: llvm.return
+llvm.func @unused_signaling_comparison_strict(%left: f32, %right: f32) {
+  %unused = llvm.intr.experimental.constrained.fcmps "oeq" %left, %right strict : f32
+  llvm.return
+}
+
+// CHECK-LABEL: llvm.func @unused_signaling_comparison_maytrap
+// CHECK: llvm.intr.experimental.constrained.fcmps oeq {{.*}} maytrap
+// CHECK: llvm.return
+llvm.func @unused_signaling_comparison_maytrap(%left: f32, %right: f32) {
+  %unused = llvm.intr.experimental.constrained.fcmps "oeq" %left, %right maytrap : f32
+  llvm.return
+}
+
+// CHECK-LABEL: llvm.func @unused_signaling_comparison_ignore
+// CHECK-NOT: llvm.intr.experimental.constrained
+// CHECK: llvm.return
+llvm.func @unused_signaling_comparison_ignore(%left: f32, %right: f32) {
+  %unused = llvm.intr.experimental.constrained.fcmps "oeq" %left, %right ignore : f32
+  llvm.return
+}
+
+// CHECK-LABEL: llvm.func @unused_conversion_strict
+// CHECK: llvm.intr.experimental.constrained.fptosi {{.*}} strict
+// CHECK: llvm.return
+llvm.func @unused_conversion_strict(%input: f64) {
+  %unused = llvm.intr.experimental.constrained.fptosi %input strict : f64 to i32
+  llvm.return
+}
+
+// CHECK-LABEL: llvm.func @unused_truncation_strict
+// CHECK: llvm.intr.experimental.constrained.fptrunc {{.*}} dynamic strict
+// CHECK: llvm.return
+llvm.func @unused_truncation_strict(%input: f64) {
+  %unused = llvm.intr.experimental.constrained.fptrunc %input dynamic strict : f64 to f32
+  llvm.return
+}
+
+// CHECK-LABEL: llvm.func @unused_vector_maytrap
+// CHECK: llvm.intr.experimental.constrained.sqrt {{.*}} upward maytrap
+// CHECK: llvm.return
+llvm.func @unused_vector_maytrap(%input: vector<4xf32>) {
+  %unused = llvm.intr.experimental.constrained.sqrt %input upward maytrap : vector<4xf32>
+  llvm.return
+}
+
+// CHECK-LABEL: llvm.func @unused_dynamic_ignore
+// CHECK-NOT: llvm.intr.experimental.constrained
+// CHECK: llvm.return
+llvm.func @unused_dynamic_ignore(%input: f64) {
+  %unused = llvm.intr.experimental.constrained.sqrt %input dynamic ignore : f64
+  llvm.return
+}
+
+// CHECK-LABEL: llvm.func @maytrap_not_cse
+// CHECK: llvm.intr.experimental.constrained.sqrt
+// CHECK: llvm.intr.experimental.constrained.sqrt
+// CHECK: llvm.return
+llvm.func @maytrap_not_cse(%input: f64) -> f64 {
+  %first = llvm.intr.experimental.constrained.sqrt %input downward maytrap : f64
+  %second = llvm.intr.experimental.constrained.sqrt %input downward maytrap : f64
+  %sum = llvm.fadd %first, %second : f64
+  llvm.return %sum : f64
+}
+
+// CHECK-LABEL: llvm.func @comparison_ignore_can_cse
+// CHECK: %[[COMPARE:.*]] = llvm.intr.experimental.constrained.fcmps
+// CHECK-NOT: llvm.intr.experimental.constrained.fcmps
+// CHECK: llvm.xor %[[COMPARE]], %[[COMPARE]]
+// CHECK: llvm.return
+llvm.func @comparison_ignore_can_cse(%left: vector<4xf32>, %right: vector<4xf32>) -> vector<4xi1> {
+  %first = llvm.intr.experimental.constrained.fcmps "olt" %left, %right ignore : vector<4xf32>
+  %second = llvm.intr.experimental.constrained.fcmps "olt" %left, %right ignore : vector<4xf32>
+  %difference = llvm.xor %first, %second : vector<4xi1>
+  llvm.return %difference : vector<4xi1>
+}

``````````

</details>


https://github.com/llvm/llvm-project/pull/234506


More information about the Mlir-commits mailing list