Skip to content

[MLIR][LLVMIR] Add an operation to handle constrained FP intrinsics#199754

Open
andykaylor wants to merge 1 commit into
llvm:mainfrom
andykaylor:llvm-constrained-fp
Open

[MLIR][LLVMIR] Add an operation to handle constrained FP intrinsics#199754
andykaylor wants to merge 1 commit into
llvm:mainfrom
andykaylor:llvm-constrained-fp

Conversation

@andykaylor
Copy link
Copy Markdown
Contributor

The LLVM dialect currently only handles a subset of all the constrained FP intrinsics defined in LLVM IR. Attempts to import LLVM IR that uses any of the others results in an assertion failure while trying to parse the metadata arguments.

This change introduces a single operation, CallConstrainedFPIntrinsicOp, that handles most of the others in a way that's analagous to the general CallIntrinsicOp but with special handling for the metadata arguments. This doesn't handle the constrained fcmp/fcmps intrinsics because they need additional argument handling. I'll add them in a follow-up change.

I'm introducing this now because I'd like to add strict FP handling to CIR, but I think this should be viewed as a temporary solution because (hopefully) the constrained intrinsics in LLVM IR will be replaced soon(ish). That is the primary reason I chose not to have individual operations for each of these. I hope we'll be able to replace this with a better representation soon.

Assisted-by: Cursor / claude-opus-4.7-thinking-xhigh

The LLVM dialect currently only handles a subset of all the constrained
FP intrinsics defined in LLVM IR. Attempts to import LLVM IR that uses
any of the others results in an assertion failure while trying to parse
the metadata arguments.

This change introduces a single operation, CallConstrainedFPIntrinsicOp,
that handles most of the others in a way that's analagous to the
general CallIntrinsicOp but with special handling for the metadata
arguments. This doesn't handle the constrained fcmp/fcmps intrinsics
because they need additional argument handling. I'll add them in a
follow-up change.

I'm introducing this now because I'd like to add strict FP handling
to CIR, but I think this should be viewed as a temporary solution
because (hopefully) the constrained intrinsics in LLVM IR will be
replaced soon(ish). That is the primary reason I chose not to have
individual operations for each of these. I hope we'll be able to
replace this with a better representation soon.

Assisted-by: Cursor / claude-opus-4.7-thinking-xhigh
@llvmorg-github-actions
Copy link
Copy Markdown

llvmorg-github-actions Bot commented May 26, 2026

@llvm/pr-subscribers-mlir-llvm

@llvm/pr-subscribers-mlir

Author: Andy Kaylor (andykaylor)

Changes

The LLVM dialect currently only handles a subset of all the constrained FP intrinsics defined in LLVM IR. Attempts to import LLVM IR that uses any of the others results in an assertion failure while trying to parse the metadata arguments.

This change introduces a single operation, CallConstrainedFPIntrinsicOp, that handles most of the others in a way that's analagous to the general CallIntrinsicOp but with special handling for the metadata arguments. This doesn't handle the constrained fcmp/fcmps intrinsics because they need additional argument handling. I'll add them in a follow-up change.

I'm introducing this now because I'd like to add strict FP handling to CIR, but I think this should be viewed as a temporary solution because (hopefully) the constrained intrinsics in LLVM IR will be replaced soon(ish). That is the primary reason I chose not to have individual operations for each of these. I hope we'll be able to replace this with a better representation soon.

Assisted-by: Cursor / claude-opus-4.7-thinking-xhigh


Patch is 25.91 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/199754.diff

9 Files Affected:

  • (modified) mlir/include/mlir/Dialect/LLVMIR/LLVMIntrinsicOps.td (+51)
  • (modified) mlir/lib/Dialect/LLVMIR/IR/LLVMDialect.cpp (+29)
  • (modified) mlir/lib/Target/LLVMIR/Dialect/LLVMIR/LLVMIRToLLVMTranslation.cpp (+112-4)
  • (modified) mlir/lib/Target/LLVMIR/Dialect/LLVMIR/LLVMToLLVMIRTranslation.cpp (+61)
  • (modified) mlir/test/Dialect/LLVMIR/invalid.mlir (+40)
  • (modified) mlir/test/Dialect/LLVMIR/roundtrip.mlir (+17)
  • (modified) mlir/test/Target/LLVMIR/Import/import-failure.ll (+20)
  • (modified) mlir/test/Target/LLVMIR/Import/intrinsic.ll (+25)
  • (modified) mlir/test/Target/LLVMIR/llvmir-intrinsics.mlir (+35)
diff --git a/mlir/include/mlir/Dialect/LLVMIR/LLVMIntrinsicOps.td b/mlir/include/mlir/Dialect/LLVMIR/LLVMIntrinsicOps.td
index 688bc19cbf18a..0785ff6ebfd49 100644
--- a/mlir/include/mlir/Dialect/LLVMIR/LLVMIntrinsicOps.td
+++ b/mlir/include/mlir/Dialect/LLVMIR/LLVMIntrinsicOps.td
@@ -596,6 +596,57 @@ def LLVM_ConstrainedFPExtIntr
   }];
 }
 
+// Generic constrained floating-point intrinsic call.
+
+def LLVM_CallConstrainedFPIntrinsicOp
+    : LLVM_Op<"intr.experimental.constrained_fp_call",
+              [Pure,
+               DeclareOpInterfaceMethods<FPExceptionBehaviorOpInterface>,
+               DeclareOpInterfaceMethods<RoundingModeOpInterface>]> {
+  let summary = "Generic call to an LLVM constrained floating-point intrinsic.";
+  let description = [{
+    Calls an LLVM constrained floating-point intrinsic by name. The intrinsic
+    name is given by the `intrin` attribute (for example
+    `"llvm.experimental.constrained.cos.f32"`). Overloaded intrinsics are
+    resolved from the MLIR operand and result types of this op.
+
+    The rounding mode operand is required for intrinsics for which
+    `llvm::Intrinsic::hasConstrainedFPRoundingModeOperand` returns true and is
+    forbidden otherwise. The exception behavior attribute is always required.
+
+    This op handles every constrained FP intrinsic that follows the standard
+    operand layout `(args..., [rounding,] exception)`. The compare variants
+    `llvm.experimental.constrained.fcmp` and
+    `llvm.experimental.constrained.fcmps` carry an additional predicate
+    metadata operand and are not supported.
+
+    Example:
+
+    ```mlir
+    %res = llvm.intr.experimental.constrained_fp_call
+        "llvm.experimental.constrained.cos.f32"(%arg)
+        towardzero ignore : (f32) -> f32
+    ```
+  }];
+
+  let arguments = (ins StrAttr:$intrin,
+                       Variadic<LLVM_Type>:$args,
+                       OptionalAttr<ValidRoundingModeAttr>:$roundingmode,
+                       FPExceptionBehaviorAttr:$fpExceptionBehavior);
+  let results = (outs LLVM_Type:$res);
+
+  let llvmBuilder = [{
+    return convertCallConstrainedFPIntrinsicOp(op, builder, moduleTranslation);
+  }];
+
+  let assemblyFormat = [{
+    $intrin `(` $args `)` ($roundingmode^)? $fpExceptionBehavior
+      attr-dict `:` functional-type($args, $res)
+  }];
+
+  let hasVerifier = 1;
+}
+
 // Intrinsics with multiple returns.
 
 class LLVM_ArithWithOverflowOp<string mnem>
diff --git a/mlir/lib/Dialect/LLVMIR/IR/LLVMDialect.cpp b/mlir/lib/Dialect/LLVMIR/IR/LLVMDialect.cpp
index 63bd9f8a3d625..5ee8f5004cf48 100644
--- a/mlir/lib/Dialect/LLVMIR/IR/LLVMDialect.cpp
+++ b/mlir/lib/Dialect/LLVMIR/IR/LLVMDialect.cpp
@@ -27,6 +27,7 @@
 #include "llvm/ADT/APFloat.h"
 #include "llvm/ADT/TypeSwitch.h"
 #include "llvm/IR/DataLayout.h"
+#include "llvm/IR/Intrinsics.h"
 #include "llvm/Support/Error.h"
 
 #include "LLVMDialectBytecode.h"
@@ -4012,6 +4013,34 @@ LogicalResult CallIntrinsicOp::verify() {
   return success();
 }
 
+//===----------------------------------------------------------------------===//
+// CallConstrainedFPIntrinsicOp
+//===----------------------------------------------------------------------===//
+
+LogicalResult CallConstrainedFPIntrinsicOp::verify() {
+  StringRef name = getIntrin();
+  llvm::Intrinsic::ID id = llvm::Intrinsic::lookupIntrinsicID(name);
+  if (!id)
+    return emitOpError() << "could not find LLVM intrinsic: " << name;
+  if (!llvm::Intrinsic::isConstrainedFPIntrinsic(id))
+    return emitOpError() << "intrinsic " << name
+                         << " is not a constrained FP intrinsic";
+  if (id == llvm::Intrinsic::experimental_constrained_fcmp ||
+      id == llvm::Intrinsic::experimental_constrained_fcmps)
+    return emitOpError() << "intrinsic " << name
+                         << " is a constrained FP compare and is not "
+                            "supported by this op";
+  bool requiresRounding =
+      llvm::Intrinsic::hasConstrainedFPRoundingModeOperand(id);
+  if (requiresRounding && !getRoundingmodeAttr())
+    return emitOpError() << "intrinsic " << name
+                         << " requires a rounding mode attribute";
+  if (!requiresRounding && getRoundingmodeAttr())
+    return emitOpError() << "intrinsic " << name
+                         << " does not take a rounding mode attribute";
+  return success();
+}
+
 void CallIntrinsicOp::build(OpBuilder &builder, OperationState &state,
                             mlir::StringAttr intrin, mlir::ValueRange args) {
   build(builder, state, /*resultTypes=*/TypeRange{}, intrin, args,
diff --git a/mlir/lib/Target/LLVMIR/Dialect/LLVMIR/LLVMIRToLLVMTranslation.cpp b/mlir/lib/Target/LLVMIR/Dialect/LLVMIR/LLVMIRToLLVMTranslation.cpp
index e9cd335835263..89105242e7180 100644
--- a/mlir/lib/Target/LLVMIR/Dialect/LLVMIR/LLVMIRToLLVMTranslation.cpp
+++ b/mlir/lib/Target/LLVMIR/Dialect/LLVMIR/LLVMIRToLLVMTranslation.cpp
@@ -22,6 +22,7 @@
 #include "llvm/IR/InlineAsm.h"
 #include "llvm/IR/Instructions.h"
 #include "llvm/IR/IntrinsicInst.h"
+#include "llvm/IR/Intrinsics.h"
 #include "llvm/IR/MemoryModelRelaxationAnnotations.h"
 
 using namespace mlir;
@@ -36,24 +37,113 @@ static constexpr StringLiteral reqdWorkGroupSizeMDName = "reqd_work_group_size";
 static constexpr StringLiteral intelReqdSubGroupSizeMDName =
     "intel_reqd_sub_group_size";
 
+/// Returns true if `id` is a constrained FP intrinsic that the generic
+/// LLVM_CallConstrainedFPIntrinsicOp can model (i.e. it has the standard
+/// trailing metadata layout: rounding mode and/or exception behavior, with no
+/// additional predicate metadata).
+static bool isGenericConstrainedFPIntrinsic(llvm::Intrinsic::ID id) {
+  if (!llvm::Intrinsic::isConstrainedFPIntrinsic(id))
+    return false;
+  // fcmp / fcmps carry an extra predicate metadata operand and are not
+  // representable by the generic op.
+  return id != llvm::Intrinsic::experimental_constrained_fcmp &&
+         id != llvm::Intrinsic::experimental_constrained_fcmps;
+}
+
+/// Returns true if `id` is a constrained FP compare intrinsic. These have a
+/// predicate metadata operand in addition to the exception behavior operand
+/// and are not currently importable, but should fail with a clean diagnostic
+/// instead of falling through to the generic intrinsic path and tripping the
+/// metadata assertion in `convertValue`.
+static bool isConstrainedFPCmpIntrinsic(llvm::Intrinsic::ID id) {
+  return id == llvm::Intrinsic::experimental_constrained_fcmp ||
+         id == llvm::Intrinsic::experimental_constrained_fcmps;
+}
+
 /// Returns true if the LLVM IR intrinsic is convertible to an MLIR LLVM dialect
-/// intrinsic. Returns false otherwise.
+/// intrinsic. Returns false otherwise. Constrained FP compare intrinsics are
+/// claimed here so that the import emits a targeted error rather than crashing
+/// in the unregistered-intrinsic fallback.
 static bool isConvertibleIntrinsic(llvm::Intrinsic::ID id) {
   static const DenseSet<unsigned> convertibleIntrinsics = {
 #include "mlir/Dialect/LLVMIR/LLVMConvertibleLLVMIRIntrinsics.inc"
   };
-  return convertibleIntrinsics.contains(id);
+  if (convertibleIntrinsics.contains(id))
+    return true;
+  return isGenericConstrainedFPIntrinsic(id) || isConstrainedFPCmpIntrinsic(id);
 }
 
 /// Returns the list of LLVM IR intrinsic identifiers that are convertible to
 /// MLIR LLVM dialect intrinsics.
 static ArrayRef<unsigned> getSupportedIntrinsicsImpl() {
-  static const SmallVector<unsigned> convertibleIntrinsics = {
+  static const SmallVector<unsigned> convertibleIntrinsics = [] {
+    SmallVector<unsigned> ids = {
 #include "mlir/Dialect/LLVMIR/LLVMConvertibleLLVMIRIntrinsics.inc"
-  };
+    };
+    // Also register the constrained FP intrinsics that fall back to the
+    // generic LLVM_CallConstrainedFPIntrinsicOp. Compare variants are
+    // registered too so the importer can emit a clean error for them instead
+    // of letting them fall through to the unregistered-intrinsic path, which
+    // would trip the metadata assertion in `convertValue`.
+    DenseSet<unsigned> seen(ids.begin(), ids.end());
+    for (unsigned id = 1; id < llvm::Intrinsic::num_intrinsics; ++id) {
+      auto intrinId = static_cast<llvm::Intrinsic::ID>(id);
+      if (seen.contains(id))
+        continue;
+      if (isGenericConstrainedFPIntrinsic(intrinId) ||
+          isConstrainedFPCmpIntrinsic(intrinId))
+        ids.push_back(id);
+    }
+    return ids;
+  }();
   return convertibleIntrinsics;
 }
 
+/// Imports a constrained FP intrinsic call as a generic
+/// LLVM_CallConstrainedFPIntrinsicOp. Splits the call's operands into value
+/// arguments and the trailing rounding-mode/exception-behavior metadata
+/// operands.
+static LogicalResult
+convertConstrainedFPIntrinsicCallOp(OpBuilder &builder, llvm::CallInst *inst,
+                                    LLVM::ModuleImport &moduleImport) {
+  llvm::Intrinsic::ID id = inst->getIntrinsicID();
+  llvm::Function *callee = inst->getCalledFunction();
+  if (!callee)
+    return failure();
+  StringRef intrinName = callee->getName();
+  bool hasRounding = llvm::Intrinsic::hasConstrainedFPRoundingModeOperand(id);
+
+  unsigned numArgs = inst->arg_size();
+  unsigned numMetadata = hasRounding ? 2 : 1;
+  if (numArgs < numMetadata)
+    return failure();
+  unsigned numValueArgs = numArgs - numMetadata;
+
+  SmallVector<Value> args;
+  args.reserve(numValueArgs);
+  for (unsigned i = 0; i < numValueArgs; ++i) {
+    FailureOr<Value> v = moduleImport.convertValue(inst->getArgOperand(i));
+    if (failed(v))
+      return failure();
+    args.push_back(*v);
+  }
+
+  RoundingModeAttr roundingMode;
+  if (hasRounding)
+    roundingMode =
+        moduleImport.matchRoundingModeAttr(inst->getArgOperand(numValueArgs));
+  FPExceptionBehaviorAttr exceptionBehavior =
+      moduleImport.matchFPExceptionBehaviorAttr(
+          inst->getArgOperand(numArgs - 1));
+
+  Type resultType = moduleImport.convertType(inst->getType());
+  auto op = CallConstrainedFPIntrinsicOp::create(
+      builder, moduleImport.translateLoc(inst->getDebugLoc()), resultType,
+      builder.getStringAttr(intrinName), args, roundingMode, exceptionBehavior);
+  moduleImport.mapValue(inst) = op.getRes();
+  return success();
+}
+
 /// Converts the LLVM intrinsic to an MLIR LLVM dialect operation if a
 /// conversion exits. Returns failure otherwise.
 static LogicalResult convertIntrinsicImpl(OpBuilder &odsBuilder,
@@ -73,6 +163,24 @@ static LogicalResult convertIntrinsicImpl(OpBuilder &odsBuilder,
       llvmOpBundles.push_back(inst->getOperandBundleAt(i));
 
 #include "mlir/Dialect/LLVMIR/LLVMIntrinsicFromLLVMIRConversions.inc"
+
+    // Fallback for constrained FP intrinsics without a dedicated MLIR op.
+    if (isGenericConstrainedFPIntrinsic(intrinsicID))
+      return convertConstrainedFPIntrinsicCallOp(odsBuilder, inst,
+                                                 moduleImport);
+
+    // Constrained FP compare intrinsics are claimed here so that we can emit
+    // a targeted error instead of falling through to convertUnregistered-
+    // Intrinsic (which would crash on the predicate metadata operand).
+    if (isConstrainedFPCmpIntrinsic(intrinsicID)) {
+      Location loc = moduleImport.translateLoc(inst->getDebugLoc());
+      StringRef intrinName = inst->getCalledFunction()
+                                 ? inst->getCalledFunction()->getName()
+                                 : StringRef("<unknown>");
+      return emitError(loc)
+             << "constrained FP compare intrinsic '" << intrinName
+             << "' is not supported by the LLVM dialect importer";
+    }
   }
 
   return failure();
diff --git a/mlir/lib/Target/LLVMIR/Dialect/LLVMIR/LLVMToLLVMIRTranslation.cpp b/mlir/lib/Target/LLVMIR/Dialect/LLVMIR/LLVMToLLVMIRTranslation.cpp
index 5474689c9b0b5..65cccbd055286 100644
--- a/mlir/lib/Target/LLVMIR/Dialect/LLVMIR/LLVMToLLVMIRTranslation.cpp
+++ b/mlir/lib/Target/LLVMIR/Dialect/LLVMIR/LLVMToLLVMIRTranslation.cpp
@@ -135,6 +135,67 @@ convertOperandBundles(OperandRangeRange bundleOperands,
   return convertOperandBundles(bundleOperands, *bundleTags, moduleTranslation);
 }
 
+/// Builder for LLVM_CallConstrainedFPIntrinsicOp. Resolves the intrinsic
+/// identifier from the `intrin` attribute, infers any overloaded types from the
+/// MLIR operand and result types, and emits an LLVM IR constrained FP call.
+static LogicalResult convertCallConstrainedFPIntrinsicOp(
+    CallConstrainedFPIntrinsicOp op, llvm::IRBuilderBase &builder,
+    LLVM::ModuleTranslation &moduleTranslation) {
+  llvm::Module *module = builder.GetInsertBlock()->getModule();
+  llvm::Intrinsic::ID id = llvm::Intrinsic::lookupIntrinsicID(op.getIntrin());
+  if (!id)
+    return mlir::emitError(op.getLoc(), "could not find LLVM intrinsic: ")
+           << op.getIntrin();
+  if (!llvm::Intrinsic::isConstrainedFPIntrinsic(id))
+    return mlir::emitError(op.getLoc(), "not a constrained FP intrinsic: ")
+           << op.getIntrin();
+  if (id == llvm::Intrinsic::experimental_constrained_fcmp ||
+      id == llvm::Intrinsic::experimental_constrained_fcmps)
+    return mlir::emitError(op.getLoc())
+           << op.getIntrin()
+           << " is a constrained FP compare and is not supported by this op";
+
+  // Build a signature matching what the intrinsic declaration looks like in
+  // LLVM IR, including the trailing metadata operands. This lets
+  // Intrinsic::isSignatureValid resolve all overloaded types.
+  SmallVector<llvm::Type *> argTys;
+  argTys.reserve(op.getArgs().size() + 2);
+  for (Type type : op.getArgs().getTypes())
+    argTys.push_back(moduleTranslation.convertType(type));
+  llvm::Type *metadataTy = llvm::Type::getMetadataTy(module->getContext());
+  if (llvm::Intrinsic::hasConstrainedFPRoundingModeOperand(id))
+    argTys.push_back(metadataTy);
+  argTys.push_back(metadataTy);
+
+  llvm::Type *resultTy = moduleTranslation.convertType(op.getRes().getType());
+  llvm::FunctionType *ft =
+      llvm::FunctionType::get(resultTy, argTys, /*isVarArg=*/false);
+
+  std::string errorMsg;
+  llvm::raw_string_ostream errorOS(errorMsg);
+  SmallVector<llvm::Type *> overloadedTys;
+  if (!llvm::Intrinsic::isSignatureValid(id, ft, overloadedTys, errorOS)) {
+    return mlir::emitError(op.getLoc(), "call intrinsic signature ")
+           << diagStr(ft) << " to constrained FP intrinsic " << op.getIntrin()
+           << " does not match any overload: " << errorMsg;
+  }
+
+  llvm::Function *fn =
+      llvm::Intrinsic::getOrInsertDeclaration(module, id, overloadedTys);
+
+  std::optional<llvm::RoundingMode> rounding;
+  if (auto roundingAttr = op.getRoundingmodeAttr())
+    rounding = moduleTranslation.translateRoundingMode(roundingAttr.getValue());
+  llvm::fp::ExceptionBehavior except =
+      moduleTranslation.translateFPExceptionBehavior(
+          op.getFpExceptionBehavior());
+
+  llvm::Value *result = builder.CreateConstrainedFPCall(
+      fn, moduleTranslation.lookupValues(op.getArgs()), "", rounding, except);
+  moduleTranslation.mapValue(op.getRes()) = result;
+  return success();
+}
+
 /// Builder for LLVM_CallIntrinsicOp
 static LogicalResult
 convertCallLLVMIntrinsicOp(CallIntrinsicOp op, llvm::IRBuilderBase &builder,
diff --git a/mlir/test/Dialect/LLVMIR/invalid.mlir b/mlir/test/Dialect/LLVMIR/invalid.mlir
index e80094df1eed2..78f3736d13674 100644
--- a/mlir/test/Dialect/LLVMIR/invalid.mlir
+++ b/mlir/test/Dialect/LLVMIR/invalid.mlir
@@ -1783,6 +1783,46 @@ llvm.func @wrong_number_of_bundle_types_intrin(%arg0: i32) -> i32 {
 
 // -----
 
+llvm.func @constrained_fp_call_unknown_intrinsic(%arg0: f32) -> f32 {
+  // expected-error@+1 {{could not find LLVM intrinsic: llvm.experimental.constrained.bogus.f32}}
+  %0 = llvm.intr.experimental.constrained_fp_call "llvm.experimental.constrained.bogus.f32"(%arg0) towardzero ignore : (f32) -> f32
+  llvm.return %0 : f32
+}
+
+// -----
+
+llvm.func @constrained_fp_call_not_constrained(%arg0: f32) -> f32 {
+  // expected-error@+1 {{intrinsic llvm.cos.f32 is not a constrained FP intrinsic}}
+  %0 = llvm.intr.experimental.constrained_fp_call "llvm.cos.f32"(%arg0) towardzero ignore : (f32) -> f32
+  llvm.return %0 : f32
+}
+
+// -----
+
+llvm.func @constrained_fp_call_fcmp_rejected(%arg0: f32) -> i1 {
+  // expected-error@+1 {{intrinsic llvm.experimental.constrained.fcmp.f32 is a constrained FP compare and is not supported by this op}}
+  %0 = llvm.intr.experimental.constrained_fp_call "llvm.experimental.constrained.fcmp.f32"(%arg0, %arg0) ignore : (f32, f32) -> i1
+  llvm.return %0 : i1
+}
+
+// -----
+
+llvm.func @constrained_fp_call_missing_rounding(%arg0: f32) -> f32 {
+  // expected-error@+1 {{intrinsic llvm.experimental.constrained.cos.f32 requires a rounding mode attribute}}
+  %0 = llvm.intr.experimental.constrained_fp_call "llvm.experimental.constrained.cos.f32"(%arg0) ignore : (f32) -> f32
+  llvm.return %0 : f32
+}
+
+// -----
+
+llvm.func @constrained_fp_call_unexpected_rounding(%arg0: f64) -> f64 {
+  // expected-error@+1 {{intrinsic llvm.experimental.constrained.maximum.f64 does not take a rounding mode attribute}}
+  %0 = llvm.intr.experimental.constrained_fp_call "llvm.experimental.constrained.maximum.f64"(%arg0, %arg0) towardzero ignore : (f64, f64) -> f64
+  llvm.return %0 : f64
+}
+
+// -----
+
 llvm.func @foo()
 llvm.func @wrong_number_of_bundle_tags() {
   %0 = llvm.mlir.constant(0 : i32) : i32
diff --git a/mlir/test/Dialect/LLVMIR/roundtrip.mlir b/mlir/test/Dialect/LLVMIR/roundtrip.mlir
index 83d1f1b8e2884..a9861ca756499 100644
--- a/mlir/test/Dialect/LLVMIR/roundtrip.mlir
+++ b/mlir/test/Dialect/LLVMIR/roundtrip.mlir
@@ -902,6 +902,23 @@ llvm.func @experimental_constrained_fptrunc(%in: f64) {
   llvm.return
 }
 
+// CHECK-LABEL: @experimental_constrained_fp_call
+llvm.func @experimental_constrained_fp_call(%s: f32, %d: f64, %p: i32) {
+  // CHECK: llvm.intr.experimental.constrained_fp_call "llvm.experimental.constrained.cos.f32"(%{{.*}}) towardzero ignore : (f32) -> f32
+  %0 = llvm.intr.experimental.constrained_fp_call
+       "llvm.experimental.constrained.cos.f32"(%s) towardzero ignore
+       : (f32) -> f32
+  // CHECK: llvm.intr.experimental.constrained_fp_call "llvm.experimental.constrained.maximum.f64"(%{{.*}}, %{{.*}}) strict : (f64, f64) -> f64
+  %1 = llvm.intr.experimental.constrained_fp_call
+       "llvm.experimental.constrained.maximum.f64"(%d, %d) strict
+       : (f64, f64) -> f64
+  // CHECK: llvm.intr.experimental.constrained_fp_call "llvm.experimental.constrained.powi.f32"(%{{.*}}, %{{.*}}) tonearest ignore : (f32, i32) -> f32
+  %2 = llvm.intr.experimental.constrained_fp_call
+       "llvm.experimental.constrained.powi.f32"(%s, %p) tonearest ignore
+       : (f32, i32) -> f32
+  llvm.return
+}
+
 // CHECK: llvm.func @tail_call_target() -> i32
 llvm.func @tail_call_target() -> i32
 
diff --git a/mlir/test/Target/LLVMIR/Import/import-failure.ll b/mlir/test/Target/LLVMIR/Import/import-failure.ll
index b468a3e95c907..86760e26c88a5 100644
--- a/mlir/test/Target/LLVMIR/Import/import-failure.ll
+++ b/mlir/test/Target/LLVMIR/Import/import-failure.ll
@@ -456,3 +456,23 @@ bb1:
 !91885 = !{!91886, !91887}
 !91886 = !{i32 10000, i64 86427, i32 1}
 !91887 = !{i32 100000, i64 86427, i32 1}
+
+; // -----
+
+; CHECK: error: constrained FP compare intrinsic 'llvm.experimental.constrained.fcmp.f32' is not supported by the LLVM dialect importer
+define i1 @constrained_fcmp(float %s) {
+  %r = call i1 @llvm.experimental.constrained.fcmp.f32(float %s, float %s, metadata !"oeq", metadata !"fpexcept.ignore")
+  ret i1 %r
+}
+
+declare i1 @llvm.experimental.constrained.fcmp.f32(float, float, metadata, metadata)
+
+; // -----
+
+; CHECK: error: constrained FP compare intrinsic 'llvm.experimental.constrained.fcmps.f32' is not supported by the LLVM dialect importer
+define i1 @constrained_fcmps(float %s) {
+  %r = call i1 @llvm.experimental.constrained.fcmps.f32(float %s, float %s, metadata !"oeq", metadata !"fpexcept.ignore")
+  ret i1 %r
+}
+
+declare i1 @llvm.experimental.constrained.fcmps.f32(float, float, metadata, metadata)
diff --git a/mlir/test/Target/LLVMIR/Import/intrinsic.ll b/mlir/test/Target/LLVMIR/Import/intrinsic.ll
index f79d09aa3d633..0a13f31089d0c 100644
--- a/mlir/test/Target/LLVMIR/Import/intrinsic.ll
+++ b/mlir/test/Target/LLVMIR/Import/intrinsic.ll
@@ -1205,6 +1205,26 @@ define void @experimental_constrained_fpext(float %s...
[truncated]

@andykaylor
Copy link
Copy Markdown
Contributor Author

My long-term goal is to align CIR and the LLVM dialect with the changes that are proposed by @nikic here: https://discourse.llvm.org/t/rfc-yet-another-strict-fp/90798

However, I expect that will take quite a while to fully take shape, and so I think we need some support for the constrained FP intrinsics in the meantime.

Copy link
Copy Markdown
Contributor

@gysit gysit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have som high-level comments and questions.

Can you quantify the number of extra intrinsics needed? If it is a single digit number we may consider adding explicit intrinsics but I doubt it.

If we go for a call operation, I would have a slight preference for reusing the existing CallIntrinsicOp. That probably requires some plumbing to import and export the metadata.

I think the steps needed to make CallIntrinsicOp work here would be:

  • Adding rounding mode and exception behavior arguments to CallIntrinsicOp
  • Add the FPExceptionBehaviorOpInterface and RoundingModeOpInterface
  • convertUnregisteredIntrinsic needs to be extended to import the extra metadata
  • convertCallLLVMIntrinsicOp needs to be extended to export the extra metadata

Alternatively, we could also go the route of your PR. However, then I would probably avoid the work call in the name and just use something like ConstrainedFPIntrinsicOp that has an enum argument that specifies the op code rather than the string attribute? Also for consistency we should then probably replace the existing intrinsics with this new "generic" intrinsic.

@andykaylor
Copy link
Copy Markdown
Contributor Author

I have som high-level comments and questions.

Can you quantify the number of extra intrinsics needed? If it is a single digit number we may consider adding explicit intrinsics but I doubt it.

There are 38 of these intrinsics that aren't currently handled.

If we go for a call operation, I would have a slight preference for reusing the existing CallIntrinsicOp. That probably requires some plumbing to import and export the metadata.

We could handle it that way. As you say, the difficulty is in representing the metadata arguments. I don't like the idea of reusing the existing CallIntrinsicOp with a caveat that some of the arguments are attached as attributes, and I don't know of a way to directly model metadata arguments in MLIR. I also don't like the idea of adding the FPExceptionBehaviorOpInterface and RoundingModeOpInterface to the general operation when it won't apply to most intrinsics.

Alternatively, we could also go the route of your PR. However, then I would probably avoid the work call in the name and just use something like ConstrainedFPIntrinsicOp that has an enum argument that specifies the op code rather than the string attribute? Also for consistency we should then probably replace the existing intrinsics with this new "generic" intrinsic.

Dropping "Call" from the name makes sense to me. I did intend to be able to support the currently supported constrained intrinsics using this new operation, but I wasn't sure if there would be any out-of-tree impact to removing the existing operations.

@gysit
Copy link
Copy Markdown
Contributor

gysit commented May 27, 2026

We could handle it that way. As you say, the difficulty is in representing the metadata arguments. I don't like the idea of reusing the existing CallIntrinsicOp with a caveat that some of the arguments are attached as attributes, and I don't know of a way to directly model metadata arguments in MLIR. I also don't like the idea of adding the FPExceptionBehaviorOpInterface and RoundingModeOpInterface to the general operation when it won't apply to most intrinsics.

The goal of CallIntrinsicOp is to be able to model any LLVM intrinsic, from that perspective I would be fine with adding more interfaces in addition to the already present FastmathFlagsInterface. That approach would rely on all metadata arguments having a unique metadata kind which may not be flexible enough for the generic CallIntrinsicOp. An alternative may be to have a helper operation that can be used to model metadata arguments (basically an operation that takes an attribute (the converted metadata) and returns an SSA value that is consumed by the CallIntrinsicOp).

Dropping "Call" from the name makes sense to me. I did intend to be able to support the currently supported constrained intrinsics using this new operation, but I wasn't sure if there would be any out-of-tree impact to removing the existing operations.

I would vote for a full replacement rather than having multiple ways of representing the same operation. The CallIntrinsicOp is the exception for that rule since it is the fallback path for unsupported intrinsics. As long as the new operation is feature complete this is hopefully ok for out-of-tree users. But it is true that the explicit operations have better ergonomics.

@andykaylor
Copy link
Copy Markdown
Contributor Author

The goal of CallIntrinsicOp is to be able to model any LLVM intrinsic, from that perspective I would be fine with adding more interfaces in addition to the already present FastmathFlagsInterface. That approach would rely on all metadata arguments having a unique metadata kind which may not be flexible enough for the generic CallIntrinsicOp.

There are other intrinsics that take metadata arguments, and llvm.fptrunc.round even takes the exact sort of rounding argument that these take. I'll explore that approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants