[CIR] Add fenv attribute to compare operations (#213072)

This adds the #cir.fenv attribute to the cir.cmp and cir.vec.cmp and
adds verfiers to enforce that the attribute is only present when the
comparison involves floating-point values.

The cir.vec.cmp operation has an existing folder which is skipped in
this change when the fenv attribute is present. In most circumstance,
this folding could still be done, but the implementation is
intentionally conservative at this point. Proper checking of the
conditions under which folding would be legal will be added later.

The cir.cmp operation does not currently have a folder.

Assisted-by: Cursor / various models
This commit is contained in:
Andy Kaylor 2026-07-30 10:55:16 -07:00 committed by GitHub
parent 4f705acc38
commit 005b3858f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 180 additions and 5 deletions

View file

@ -2500,7 +2500,8 @@ def CIR_CmpOpKind : CIR_I32EnumAttr<"CmpOpKind", "compare operation kind", [
I32EnumAttrCase<"uno", 7>
]>;
def CIR_CmpOp : CIR_Op<"cmp", [Pure, SameTypeOperands]> {
def CIR_CmpOp : CIR_Op<"cmp",
!listconcat([SameTypeOperands], CIR_FenvOpTraits)> {
let summary = "Compare two values and produce a boolean result";
let description = [{
The `cir.cmp` operation compares two operands of the same type and produces
@ -2524,16 +2525,22 @@ def CIR_CmpOp : CIR_Op<"cmp", [Pure, SameTypeOperands]> {
The `one` and `uno` predicates are floating-point specific: `one` is
ordered not-equal (false for NaN), `uno` tests if either operand is NaN.
The optional `fenv` attribute describes constraints on the floating-point
handling of the operation. It is only valid for floating-point
comparisons.
```
%0 = cir.cmp gt %1, %2 : !s32i
%1 = cir.cmp eq %a, %b : !cir.ptr<!u8i>
%2 = cir.cmp lt %x, %y : !cir.float {fenv = #cir.fenv<>}
```
}];
let arguments = (ins
CIR_CmpOpKind:$kind,
CIR_ComparableType:$lhs,
CIR_ComparableType:$rhs
CIR_ComparableType:$rhs,
OptionalAttr<CIR_FenvAttr>:$fenv
);
let results = (outs CIR_BoolType:$result);
@ -2542,6 +2549,18 @@ def CIR_CmpOp : CIR_Op<"cmp", [Pure, SameTypeOperands]> {
$kind $lhs `,` $rhs `:` type($lhs) attr-dict
}];
let builders = [
OpBuilder<(ins "cir::CmpOpKind":$kind, "mlir::Value":$lhs,
"mlir::Value":$rhs), [{
build($_builder, $_state, kind, lhs, rhs, cir::FenvAttr{});
}]>,
OpBuilder<(ins "mlir::Type":$result, "cir::CmpOpKind":$kind,
"mlir::Value":$lhs, "mlir::Value":$rhs), [{
build($_builder, $_state, result, kind, lhs, rhs, cir::FenvAttr{});
}]>
];
let hasVerifier = 1;
let isLLVMLoweringRecursive = true;
let hasCXXABILowering = true;
}
@ -5716,7 +5735,8 @@ def CIR_VecExtractOp : CIR_Op<"vec.extract", [
// VecCmpOp
//===----------------------------------------------------------------------===//
def CIR_VecCmpOp : CIR_Op<"vec.cmp", [Pure, SameTypeOperands]> {
def CIR_VecCmpOp : CIR_Op<"vec.cmp",
!listconcat([SameTypeOperands], CIR_FenvOpTraits)> {
let summary = "Compare two vectors";
let description = [{
The `cir.vec.cmp` operation does an element-wise comparison of two vectors
@ -5724,16 +5744,24 @@ def CIR_VecCmpOp : CIR_Op<"vec.cmp", [Pure, SameTypeOperands]> {
whose element type is the signed integral type that is the same size as the
element type of the operands. The values in the result are 0 or -1.
The optional `fenv` attribute describes constraints on the floating-point
handling of the operation. It is only valid for floating-point vector
comparisons.
```
%eq = cir.vec.cmp(eq, %vec_a, %vec_b) : !cir.vector<4 x !s32i>, !cir.vector<4 x !s32i>
%lt = cir.vec.cmp(lt, %vec_a, %vec_b) : !cir.vector<4 x !s32i>, !cir.vector<4 x !s32i>
%gt = cir.vec.cmp(gt, %va, %vb) : !cir.vector<4 x !cir.float>, !cir.vector<4 x !s32i> {
fenv = #cir.fenv<>
}
```
}];
let arguments = (ins
CIR_CmpOpKind:$kind,
CIR_VectorType:$lhs,
CIR_VectorType:$rhs
CIR_VectorType:$rhs,
OptionalAttr<CIR_FenvAttr>:$fenv
);
let results = (outs CIR_VectorType:$result);
@ -5743,6 +5771,14 @@ def CIR_VecCmpOp : CIR_Op<"vec.cmp", [Pure, SameTypeOperands]> {
qualified(type($result)) attr-dict
}];
let builders = [
OpBuilder<(ins "mlir::Type":$result, "cir::CmpOpKind":$kind,
"mlir::Value":$lhs, "mlir::Value":$rhs), [{
build($_builder, $_state, result, kind, lhs, rhs, cir::FenvAttr{});
}]>
];
let hasVerifier = 1;
let hasFolder = 1;
}

View file

@ -3536,11 +3536,37 @@ OpFoldResult cir::VecExtractOp::fold(FoldAdaptor adaptor) {
return elements[index];
}
//===----------------------------------------------------------------------===//
// CmpOp
//===----------------------------------------------------------------------===//
LogicalResult cir::CmpOp::verify() {
if (getFenvAttr() && !cir::isAnyFloatingPointType(getLhs().getType()))
return emitOpError()
<< "'fenv' is only valid for floating-point comparisons";
return success();
}
//===----------------------------------------------------------------------===//
// VecCmpOp
//===----------------------------------------------------------------------===//
LogicalResult cir::VecCmpOp::verify() {
if (getFenvAttr() && !cir::isFPOrVectorOfFPType(getLhs().getType()))
return emitOpError()
<< "'fenv' is only valid for floating-point comparisons";
return success();
}
OpFoldResult cir::VecCmpOp::fold(FoldAdaptor adaptor) {
// Do not fold when fenv is present.
// TODO(cir): This is overly conservative. We could fold comparisons as long
// as we can prove that the operation wouldn't raise exceptions or
// when the fenv attribute does not require strict exception
// semantics.
if (getFenvAttr())
return {};
auto lhsVecAttr =
mlir::dyn_cast_if_present<cir::ConstVectorAttr>(adaptor.getLhs());
auto rhsVecAttr =

View file

@ -104,3 +104,24 @@ cir.func @cast_fenv(%a: !cir.float, %b: !cir.double, %i: !s32i) {
%3 = cir.cast float_to_bool %a : !cir.float -> !cir.bool
cir.return
}
// CHECK-LABEL: cir.func @cmp_fenv
cir.func @cmp_fenv(%a: !cir.float, %b: !cir.float,
%va: !cir.vector<4 x !cir.float>,
%vb: !cir.vector<4 x !cir.float>) {
// CHECK: cir.cmp lt %{{.*}}, %{{.*}} : !cir.float {fenv = #cir.fenv<except_mode = unmasked>}
%0 = cir.cmp lt %a, %b : !cir.float {fenv = #cir.fenv<except_mode = unmasked>}
// CHECK: cir.cmp eq %{{.*}}, %{{.*}} : !cir.float {fenv = #cir.fenv<>}
%1 = cir.cmp eq %a, %b : !cir.float {fenv = #cir.fenv<>}
// CHECK: cir.vec.cmp(gt, %{{.*}}, %{{.*}}) : !cir.vector<4 x !cir.float>, !cir.vector<4 x !s32i> {fenv = #cir.fenv<strict_except = true>}
%2 = cir.vec.cmp(gt, %va, %vb) : !cir.vector<4 x !cir.float>, !cir.vector<4 x !s32i> {
fenv = #cir.fenv<strict_except = true>
}
// CHECK: cir.cmp ne %{{.*}}, %{{.*}} : !cir.float
// CHECK-NOT: fenv
%3 = cir.cmp ne %a, %b : !cir.float
// CHECK: cir.vec.cmp(le, %{{.*}}, %{{.*}}) : !cir.vector<4 x !cir.float>, !cir.vector<4 x !s32i>
// CHECK-NOT: fenv
%4 = cir.vec.cmp(le, %va, %vb) : !cir.vector<4 x !cir.float>, !cir.vector<4 x !s32i>
cir.return
}

View file

@ -0,0 +1,39 @@
// RUN: cir-opt %s -verify-diagnostics -split-input-file
!s32i = !cir.int<s, 32>
module {
cir.func @fenv_on_int_cmp(%a: !s32i, %b: !s32i) {
// expected-error@+1 {{'fenv' is only valid for floating-point comparisons}}
%0 = cir.cmp eq %a, %b : !s32i {fenv = #cir.fenv<>}
cir.return
}
}
// -----
!s32i = !cir.int<s, 32>
module {
cir.func @fenv_on_ptr_cmp(%a: !cir.ptr<!s32i>, %b: !cir.ptr<!s32i>) {
// expected-error@+1 {{'fenv' is only valid for floating-point comparisons}}
%0 = cir.cmp ne %a, %b : !cir.ptr<!s32i> {fenv = #cir.fenv<>}
cir.return
}
}
// -----
!s32i = !cir.int<s, 32>
module {
cir.func @fenv_on_int_vec_cmp(%a: !cir.vector<4 x !s32i>,
%b: !cir.vector<4 x !s32i>) {
// expected-error@+1 {{'fenv' is only valid for floating-point comparisons}}
%0 = cir.vec.cmp(eq, %a, %b) : !cir.vector<4 x !s32i>,
!cir.vector<4 x !s32i> {
fenv = #cir.fenv<>
}
cir.return
}
}

View file

@ -292,3 +292,34 @@ module {
// CHECK-SAME: #cir.int<-1> : !cir.int<s, 1>, #cir.int<-1> : !cir.int<s, 1>]> : !cir.vector<4 x !cir.int<s, 1>>
// CHECK-NEXT: cir.return %[[RES]] : !cir.vector<4 x !cir.int<s, 1>>
}
// -----
!s32i = !cir.int<s, 32>
module {
cir.func @no_fold_cmp_vector_with_fenv() -> !cir.vector<4 x !s32i> {
%vec_1 = cir.const #cir.const_vector<[
#cir.fp<0x7FC00000> : !cir.float,
#cir.fp<2.000000e+00> : !cir.float,
#cir.fp<3.000000e+00> : !cir.float,
#cir.fp<4.000000e+00> : !cir.float
]> : !cir.vector<4 x !cir.float>
%vec_2 = cir.const #cir.const_vector<[
#cir.fp<5.000000e+00> : !cir.float,
#cir.fp<6.000000e+00> : !cir.float,
#cir.fp<7.000000e+00> : !cir.float,
#cir.fp<8.000000e+00> : !cir.float
]> : !cir.vector<4 x !cir.float>
%new_vec = cir.vec.cmp(lt, %vec_1, %vec_2) :
!cir.vector<4 x !cir.float>, !cir.vector<4 x !s32i> {
fenv = #cir.fenv<strict_except = true>
}
cir.return %new_vec : !cir.vector<4 x !s32i>
}
// CHECK-LABEL: cir.func{{.*}} @no_fold_cmp_vector_with_fenv
// CHECK: cir.vec.cmp(lt, %{{.*}}, %{{.*}}) :
// CHECK-SAME: !cir.vector<4 x !cir.float>, !cir.vector<4 x !s32i>
// CHECK-SAME: {fenv = #cir.fenv<strict_except = true>}
}

View file

@ -64,7 +64,9 @@ protected:
TEST_F(CIRFenvOpTest, MemoryEffects) {
OwningOpRef<ModuleOp> module = parse(R"CIR(
!s32i = !cir.int<s, 32>
cir.func @f(%a: !cir.float, %b: !cir.float, %c: !cir.float) {
cir.func @f(%a: !cir.float, %b: !cir.float, %c: !cir.float,
%va: !cir.vector<4 x !cir.float>,
%vb: !cir.vector<4 x !cir.float>) {
%0 = cir.fadd %a, %b : !cir.float
%1 = cir.fadd %a, %b : !cir.float {fenv = #cir.fenv<>}
%2 = cir.sqrt %a : !cir.float {fenv = #cir.fenv<>}
@ -75,6 +77,12 @@ TEST_F(CIRFenvOpTest, MemoryEffects) {
%7 = cir.lround %a : !cir.float -> !s32i {fenv = #cir.fenv<>}
%8 = cir.cast floating %a : !cir.float -> !cir.double
%9 = cir.cast floating %a : !cir.float -> !cir.double {fenv = #cir.fenv<>}
%10 = cir.cmp lt %a, %b : !cir.float
%11 = cir.cmp lt %a, %b : !cir.float {fenv = #cir.fenv<>}
%12 = cir.vec.cmp(eq, %va, %vb) : !cir.vector<4 x !cir.float>, !cir.vector<4 x !s32i>
%13 = cir.vec.cmp(eq, %va, %vb) : !cir.vector<4 x !cir.float>, !cir.vector<4 x !s32i> {
fenv = #cir.fenv<>
}
cir.return
}
)CIR");
@ -115,6 +123,20 @@ TEST_F(CIRFenvOpTest, MemoryEffects) {
EXPECT_TRUE(isMemoryEffectFree(castOps[0]));
expectFenvReadAndWrite(castOps[1]);
EXPECT_FALSE(isMemoryEffectFree(castOps[1]));
SmallVector<cir::CmpOp> cmpOps = findOps<cir::CmpOp>(*module);
ASSERT_EQ(cmpOps.size(), 2u);
EXPECT_TRUE(getEffects(cmpOps[0]).empty());
EXPECT_TRUE(isMemoryEffectFree(cmpOps[0]));
expectFenvReadAndWrite(cmpOps[1]);
EXPECT_FALSE(isMemoryEffectFree(cmpOps[1]));
SmallVector<cir::VecCmpOp> vecCmpOps = findOps<cir::VecCmpOp>(*module);
ASSERT_EQ(vecCmpOps.size(), 2u);
EXPECT_TRUE(getEffects(vecCmpOps[0]).empty());
EXPECT_TRUE(isMemoryEffectFree(vecCmpOps[0]));
expectFenvReadAndWrite(vecCmpOps[1]);
EXPECT_FALSE(isMemoryEffectFree(vecCmpOps[1]));
}
TEST_F(CIRFenvOpTest, Speculatability) {