[flang-commits] [flang] [flang] Add fir.assert and its LLVM code generation (PR #231245)

via flang-commits flang-commits at lists.llvm.org
Fri Oct 16 00:57:03 PDT 2026


https://github.com/jeanPerier updated https://github.com/llvm/llvm-project/pull/231245

>From 23579f450aafb5a034c0d148b30e69162abe6660 Mon Sep 17 00:00:00 2001
From: Jean Perier <jperier at nvidia.com>
Date: Mon, 12 Oct 2026 09:58:43 -0700
Subject: [PATCH 1/2] [flang] Add fir.assert and its LLVM lowering

Introduce a non-speculatable assertion with typed guarded results and a
write effect on a non-addressable Fortran runtime resource. Verify its
message and diagnostic operands, and erase constant-true assertions.
Keep attribute and optionality queries able to inspect guarded values
without making rewriting operations transparent to the guard.

Lower assertions to a weighted branch, a noreturn runtime call on failure,
and guarded operands on success. Pack larger diagnostic payloads into an
array on the failure path and preserve source locations and device symbol
scope. Add dialect, code generation, effect, and variable-query tests.

This introduces the operation without emitting it from source lowering.
---
 .../flang/Optimizer/Dialect/FIRAttr.td        |  19 +++
 .../include/flang/Optimizer/Dialect/FIROps.h  |   7 +
 .../include/flang/Optimizer/Dialect/FIROps.td |  45 ++++++
 flang/lib/Optimizer/CodeGen/CodeGen.cpp       | 146 +++++++++++++++++-
 flang/lib/Optimizer/Dialect/FIROps.cpp        |  77 ++++++++-
 flang/test/Fir/assert-array-codegen.fir       |  75 +++++++++
 flang/test/Fir/assert-codegen.fir             |  68 ++++++++
 flang/test/Fir/assert-invalid.fir             |  71 +++++++++
 flang/test/Fir/assert-memory.fir              |  45 ++++++
 flang/test/Fir/assert-no-fold-through.fir     |  16 ++
 flang/test/Fir/assert.fir                     |  72 +++++++++
 .../Optimizer/FortranVariableTest.cpp         |  66 ++++++++
 12 files changed, 703 insertions(+), 4 deletions(-)
 create mode 100644 flang/test/Fir/assert-array-codegen.fir
 create mode 100644 flang/test/Fir/assert-codegen.fir
 create mode 100644 flang/test/Fir/assert-invalid.fir
 create mode 100644 flang/test/Fir/assert-memory.fir
 create mode 100644 flang/test/Fir/assert-no-fold-through.fir
 create mode 100644 flang/test/Fir/assert.fir

diff --git a/flang/include/flang/Optimizer/Dialect/FIRAttr.td b/flang/include/flang/Optimizer/Dialect/FIRAttr.td
index 88d7964768b5a9..581c831c8985e9 100644
--- a/flang/include/flang/Optimizer/Dialect/FIRAttr.td
+++ b/flang/include/flang/Optimizer/Dialect/FIRAttr.td
@@ -189,6 +189,25 @@ def fir_ReduceAttr : fir_Attr<"Reduce"> {
 // mlir::SideEffects::Resource for modelling operations which add debugging information
 def DebuggingResource : Resource<"::fir::DebuggingResource">;
 
+def FortranRuntimeResource : Resource<"::fir::FortranRuntimeResource">;
+
+def fir_RuntimeCheckKind : I32EnumAttr<"RuntimeCheckKind",
+    "Fortran runtime check category", [
+      I32EnumAttrCase<"status", 0>,
+      I32EnumAttrCase<"conformance", 1>,
+      I32EnumAttrCase<"argument", 2>,
+      I32EnumAttrCase<"bounds", 3>,
+      I32EnumAttrCase<"pointer", 4>,
+      I32EnumAttrCase<"doLoop", 5, "do">,
+      I32EnumAttrCase<"bits", 6>,
+      I32EnumAttrCase<"divide", 7>
+    ]> {
+  let genSpecializedAttr = 0;
+  let cppNamespace = "::fir";
+}
+def fir_RuntimeCheckKindAttr
+    : EnumAttr<FIROpsDialect, fir_RuntimeCheckKind, "runtime_check_kind">;
+
 def fir_LowerBoundModifierAttribute : I32EnumAttr<
     "LowerBoundModifierAttribute",
     "Describes how to modify lower bounds",
diff --git a/flang/include/flang/Optimizer/Dialect/FIROps.h b/flang/include/flang/Optimizer/Dialect/FIROps.h
index 9d771b1e606043..5ea6cb81587ba1 100644
--- a/flang/include/flang/Optimizer/Dialect/FIROps.h
+++ b/flang/include/flang/Optimizer/Dialect/FIROps.h
@@ -54,6 +54,13 @@ struct DebuggingResource
   bool isAddressable() const override { return false; }
 };
 
+/// Runtime checks can terminate execution, but do not access user memory.
+struct FortranRuntimeResource
+    : public mlir::SideEffects::Resource::Base<FortranRuntimeResource> {
+  mlir::StringRef getName() const final { return "FortranRuntimeResource"; }
+  bool isAddressable() const override { return false; }
+};
+
 /// Model operations which read from/write to volatile memory
 struct VolatileMemoryResource
     : public mlir::SideEffects::Resource::Base<VolatileMemoryResource> {
diff --git a/flang/include/flang/Optimizer/Dialect/FIROps.td b/flang/include/flang/Optimizer/Dialect/FIROps.td
index e6cd908dfab556..5a06a570f9ef4c 100644
--- a/flang/include/flang/Optimizer/Dialect/FIROps.td
+++ b/flang/include/flang/Optimizer/Dialect/FIROps.td
@@ -81,6 +81,51 @@ def AnyRefOfConstantSizeAggregateType : TypeConstraint<
 // Memory SSA operations
 //===----------------------------------------------------------------------===//
 
+def fir_AssertOp : fir_Op<"assert", [
+    AttrSizedOperandSegments,
+    TypesMatchWith<"result types match guarded operand types", "guarded",
+                   "results", "$_self">,
+    MemoryEffects<[MemWrite<FortranRuntimeResource>]>]> {
+  let summary = "check a condition and guard values used by subsequent operations";
+  let description = [{
+    If `condition` is true, each result equals the corresponding `guarded`
+    operand. Otherwise, execution terminates through the Fortran runtime,
+    reporting `message` at the operation's source location. The message may
+    contain `%lld` directives, one for each integer or index operand
+    in `values`, and `%%` for a literal percent sign. Diagnostic values are
+    interpreted as signed integers and must fit in 64 bits.
+
+    Consumers of the results depend on the check. The operation is deliberately
+    not speculatable or a view: looking through it in a rewrite would discard
+    that dependence. Analyses may inspect the guarded operands. Its write to
+    a non-addressable runtime resource preserves checks without obstructing
+    memory optimizations.
+
+    Guard array bases and scalar operands used by the protected computation.
+    FIR users of shape and slice metadata may require direct constructor
+    operands; guard the underlying extents or indices before constructing that
+    metadata instead.
+  }];
+  let arguments = (ins I1:$condition, fir_RuntimeCheckKindAttr:$kind,
+                       StrAttr:$message,
+                       Variadic<AnySignlessIntegerOrIndex>:$values,
+                       Variadic<AnyType>:$guarded);
+  let results = (outs Variadic<AnyType>:$results);
+  let assemblyFormat = [{
+    $condition `,` enum($kind) `,` $message
+    (`values` `(` $values^ `:` type($values) `)`)?
+    (`guard` `(` $guarded^ `:` type($guarded) `)`)? attr-dict
+  }];
+  let builders = [OpBuilder<(ins "mlir::Value":$condition,
+      "fir::RuntimeCheckKind":$kind, "llvm::StringRef":$message,
+      "mlir::ValueRange":$values, "mlir::ValueRange":$guarded), [{
+    build($_builder, $_state, guarded.getTypes(), condition, kind, message,
+          values, guarded);
+  }]>];
+  let hasVerifier = 1;
+  let hasCanonicalizer = 1;
+}
+
 def fir_AllocaOp : fir_Op<"alloca", [
   AttrSizedOperandSegments,
   DeclareOpInterfaceMethods<PromotableAllocationOpInterface>
diff --git a/flang/lib/Optimizer/CodeGen/CodeGen.cpp b/flang/lib/Optimizer/CodeGen/CodeGen.cpp
index d72b1fb4a1d783..161e7e2ed1254e 100644
--- a/flang/lib/Optimizer/CodeGen/CodeGen.cpp
+++ b/flang/lib/Optimizer/CodeGen/CodeGen.cpp
@@ -4415,6 +4415,150 @@ struct UnreachableOpConversion
   }
 };
 
+/// Prefer the original source of an inlined operation to its call site.
+static mlir::FileLineColLoc findAssertSourceLocation(mlir::Location loc) {
+  if (auto file = mlir::dyn_cast<mlir::FileLineColLoc>(loc))
+    return file;
+  if (auto call = mlir::dyn_cast<mlir::CallSiteLoc>(loc)) {
+    if (auto file = findAssertSourceLocation(call.getCallee()))
+      return file;
+    return findAssertSourceLocation(call.getCaller());
+  }
+  if (auto fused = mlir::dyn_cast<mlir::FusedLoc>(loc))
+    for (mlir::Location nested : fused.getLocations())
+      if (auto file = findAssertSourceLocation(nested))
+        return file;
+  if (auto named = mlir::dyn_cast<mlir::NameLoc>(loc))
+    return findAssertSourceLocation(named.getChildLoc());
+  return {};
+}
+
+struct AssertOpConversion : public fir::FIROpConversion<fir::AssertOp> {
+  using FIROpConversion::FIROpConversion;
+
+  llvm::LogicalResult
+  matchAndRewrite(fir::AssertOp op, OpAdaptor adaptor,
+                  mlir::ConversionPatternRewriter &rewriter) const override {
+    mlir::Location loc = op.getLoc();
+    // A device function's strings and runtime declaration must remain in its
+    // GPU module when host and device modules are subsequently separated.
+    mlir::Operation *scope = mlir::SymbolTable::getNearestSymbolTable(op);
+    auto ptrTy = getLlvmPtrType(op.getContext());
+    auto i64Ty = rewriter.getI64Type();
+    auto countTy = rewriter.getIntegerType(lowerTy().getPointerBitwidth(0));
+    bool useArray = adaptor.getValues().size() > 4;
+    llvm::SmallVector<mlir::Type> argumentTypes{ptrTy, ptrTy,
+                                                rewriter.getI32Type()};
+    if (useArray)
+      argumentTypes.append({countTy, ptrTy});
+    else
+      argumentTypes.append(4, i64Ty);
+    auto functionType = mlir::LLVM::LLVMFunctionType::get(
+        mlir::LLVM::LLVMVoidType::get(op.getContext()), argumentTypes);
+    llvm::StringRef runtimeName =
+        useArray ? "_FortranAReportFatalUserErrorValueArray"
+                 : "_FortranAReportFatalUserErrorValues";
+    mlir::Operation *declaration =
+        mlir::SymbolTable::lookupSymbolIn(scope, runtimeName);
+    if (declaration) {
+      if (auto function = mlir::dyn_cast<mlir::LLVM::LLVMFuncOp>(declaration)) {
+        if (function.getFunctionType() != functionType)
+          return op.emitOpError("incompatible runtime function declaration");
+        rewriter.modifyOpInPlace(function, [&] { function.setNoreturn(true); });
+      } else if (auto function =
+                     mlir::dyn_cast<mlir::func::FuncOp>(declaration)) {
+        if (function.getNumResults() != 0 ||
+            function.getNumArguments() != functionType.getNumParams())
+          return op.emitOpError("incompatible runtime function declaration");
+        for (auto [input, expected] : llvm::zip_equal(
+                 function.getArgumentTypes(), functionType.getParams()))
+          if (convertType(input) != expected)
+            return op.emitOpError("incompatible runtime function declaration");
+        rewriter.modifyOpInPlace(function, [&] {
+          function->setAttr("llvm.noreturn", rewriter.getUnitAttr());
+        });
+      } else {
+        return op.emitOpError("runtime function name conflicts with a symbol");
+      }
+    } else {
+      mlir::OpBuilder::InsertionGuard guard(rewriter);
+      rewriter.setInsertionPointToStart(&scope->getRegion(0).front());
+      auto function = mlir::LLVM::LLVMFuncOp::create(rewriter, loc, runtimeName,
+                                                     functionType);
+      function.setNoreturn(true);
+    }
+
+    mlir::Block *before = op->getBlock();
+    mlir::Block *continuation =
+        rewriter.splitBlock(before, mlir::Block::iterator(op));
+    mlir::Block *failure = rewriter.createBlock(before->getParent());
+
+    auto stringAddress = [&](llvm::StringRef text) -> mlir::Value {
+      std::string terminated = text.str() + '\0';
+      std::string name = fir::factory::uniqueCGIdent("assert", terminated);
+      if (!mlir::SymbolTable::lookupSymbolIn(scope, name)) {
+        mlir::OpBuilder::InsertionGuard guard(rewriter);
+        rewriter.setInsertionPointToStart(&scope->getRegion(0).front());
+        auto arrayTy = mlir::LLVM::LLVMArrayType::get(rewriter.getI8Type(),
+                                                      terminated.size());
+        mlir::LLVM::GlobalOp::create(rewriter, loc, arrayTy, /*constant=*/true,
+                                     mlir::LLVM::Linkage::Internal, name,
+                                     rewriter.getStringAttr(terminated));
+      }
+      return mlir::LLVM::AddressOfOp::create(rewriter, loc, ptrTy, name);
+    };
+
+    mlir::Value message = stringAddress(op.getMessage());
+    mlir::FileLineColLoc source = findAssertSourceLocation(loc);
+    mlir::Value filename =
+        source ? stringAddress(source.getFilename().getValue())
+               : mlir::LLVM::ZeroOp::create(rewriter, loc, ptrTy);
+    mlir::Value line =
+        genI32Constant(loc, rewriter, source ? source.getLine() : 0);
+    llvm::SmallVector<mlir::Value> arguments{message, filename, line};
+    if (useArray) {
+      // Materialize diagnostic storage only on the failure path. Keep the
+      // values in SSA form until here so they add no memory dependencies to
+      // the high-level assertion.
+      mlir::Value count = fir::genConstantIndex(loc, countTy, rewriter,
+                                                adaptor.getValues().size());
+      auto storagePtrTy =
+          getLlvmPtrType(op.getContext(), getAllocaAddressSpace(rewriter));
+      mlir::Value storage = mlir::LLVM::AllocaOp::create(
+          rewriter, loc, storagePtrTy, i64Ty, count);
+      for (auto [index, value] : llvm::enumerate(adaptor.getValues())) {
+        mlir::Value offset =
+            fir::genConstantIndex(loc, countTy, rewriter, index);
+        mlir::Value address = mlir::LLVM::GEPOp::create(
+            rewriter, loc, storagePtrTy, i64Ty, storage,
+            llvm::ArrayRef<mlir::LLVM::GEPArg>{offset});
+        mlir::LLVM::StoreOp::create(
+            rewriter, loc, integerCast(loc, rewriter, i64Ty, value), address);
+      }
+      if (storagePtrTy != ptrTy)
+        storage =
+            mlir::LLVM::AddrSpaceCastOp::create(rewriter, loc, ptrTy, storage);
+      arguments.append({count, storage});
+    } else {
+      for (mlir::Value value : adaptor.getValues())
+        arguments.push_back(integerCast(loc, rewriter, i64Ty, value));
+      while (arguments.size() < 7)
+        arguments.push_back(mlir::LLVM::ConstantOp::create(
+            rewriter, loc, i64Ty, rewriter.getI64IntegerAttr(0)));
+    }
+    mlir::LLVM::CallOp::create(rewriter, loc, functionType, runtimeName,
+                               arguments);
+    mlir::LLVM::UnreachableOp::create(rewriter, loc);
+
+    rewriter.setInsertionPointToEnd(before);
+    mlir::LLVM::CondBrOp::create(rewriter, loc, adaptor.getCondition(),
+                                 continuation, mlir::ValueRange{}, failure,
+                                 mlir::ValueRange{}, std::make_pair(2000u, 1u));
+    rewriter.replaceOp(op, adaptor.getGuarded());
+    return mlir::success();
+  }
+};
+
 /// `fir.is_present` -->
 /// ```
 ///  %0 = llvm.mlir.constant(0 : i64)
@@ -5193,7 +5337,7 @@ void fir::populateFIRToLLVMConversionPatterns(
       StoreOpConversion, StringLitOpConversion, SubcOpConversion,
       TypeDescOpConversion, TypeInfoOpConversion, UnboxCharOpConversion,
       UnboxProcOpConversion, UndefOpConversion, UnreachableOpConversion,
-      UseStmtOpConversion, ModuleDebugImportsOpConversion,
+      AssertOpConversion, UseStmtOpConversion, ModuleDebugImportsOpConversion,
       XArrayCoorOpConversion, XEmboxOpConversion, XReboxOpConversion,
       ZeroOpConversion>(converter, options);
 
diff --git a/flang/lib/Optimizer/Dialect/FIROps.cpp b/flang/lib/Optimizer/Dialect/FIROps.cpp
index 606d47631488bb..4f3ab9984f99e8 100644
--- a/flang/lib/Optimizer/Dialect/FIROps.cpp
+++ b/flang/lib/Optimizer/Dialect/FIROps.cpp
@@ -56,6 +56,59 @@ bool fir::useStrictVolatileVerification() {
   return clUseStrictVolatileVerification;
 }
 
+//===----------------------------------------------------------------------===//
+// AssertOp
+//===----------------------------------------------------------------------===//
+
+llvm::LogicalResult fir::AssertOp::verify() {
+  if (getGuarded().getTypes() != getResultTypes())
+    return emitOpError("result types must match guarded operand types");
+  for (mlir::Type type : getValues().getTypes())
+    if (auto integer = mlir::dyn_cast<mlir::IntegerType>(type))
+      if (integer.getWidth() > 64)
+        return emitOpError("diagnostic integers must be at most 64 bits wide");
+
+  llvm::StringRef message = getMessage();
+  if (message.contains('\0'))
+    return emitOpError("message must not contain an embedded null character");
+  unsigned directives = 0;
+  while (!message.empty()) {
+    size_t percent = message.find('%');
+    if (percent == llvm::StringRef::npos)
+      break;
+    message = message.drop_front(percent + 1);
+    if (message.consume_front("%"))
+      continue;
+    if (!message.consume_front("lld"))
+      return emitOpError("message supports only %lld and %% format directives");
+    ++directives;
+  }
+  if (directives != getValues().size())
+    return emitOpError(
+        "number of %lld directives must match diagnostic values");
+  return mlir::success();
+}
+
+namespace {
+struct RemoveTrueAssert : mlir::OpRewritePattern<fir::AssertOp> {
+  using OpRewritePattern::OpRewritePattern;
+
+  llvm::LogicalResult
+  matchAndRewrite(fir::AssertOp op,
+                  mlir::PatternRewriter &rewriter) const override {
+    if (!mlir::matchPattern(op.getCondition(), mlir::m_One()))
+      return mlir::failure();
+    rewriter.replaceOp(op, op.getGuarded());
+    return mlir::success();
+  }
+};
+} // namespace
+
+void fir::AssertOp::getCanonicalizationPatterns(
+    mlir::RewritePatternSet &patterns, mlir::MLIRContext *context) {
+  patterns.add<RemoveTrueAssert>(context);
+}
+
 static void propagateAttributes(mlir::Operation *fromOp,
                                 mlir::Operation *toOp) {
   if (!fromOp || !toOp)
@@ -247,6 +300,13 @@ bool fir::mayBeAbsentBox(mlir::Value val) {
     if (auto varIface = mlir::dyn_cast<fir::FortranVariableOpInterface>(defOp))
       return varIface.isOptional();
 
+    if (auto assertOp = mlir::dyn_cast<fir::AssertOp>(defOp)) {
+      val =
+          assertOp
+              .getGuarded()[mlir::cast<mlir::OpResult>(val).getResultNumber()];
+      continue;
+    }
+
     // Check for fir.embox and fir.rebox before checking for
     // FortranObjectViewOpInterface, which they support.
     // A box created by fir.embox/fir.rebox/fir.rebox_assumed_rank is only
@@ -6104,10 +6164,21 @@ valueCheckFirAttributes(mlir::Value value,
   };
   // If this is a fir.box that was loaded, the fir attributes will be on the
   // related fir.ref<fir.box> creation.
-  if (mlir::isa<fir::BoxType>(value.getType()))
-    if (auto definingOp = value.getDefiningOp())
-      if (auto loadOp = mlir::dyn_cast<fir::LoadOp>(definingOp))
+  // This is an attribute query only. Do not make rewriting helpers transparent
+  // to guards: replacing a guarded use with its origin loses the check.
+  while (true) {
+    if (auto assertOp = value.getDefiningOp<fir::AssertOp>()) {
+      value = assertOp.getGuarded()[mlir::cast<mlir::OpResult>(value)
+                                        .getResultNumber()];
+      continue;
+    }
+    if (mlir::isa<fir::BoxType>(value.getType()))
+      if (auto loadOp = value.getDefiningOp<fir::LoadOp>()) {
         value = loadOp.getMemref();
+        continue;
+      }
+    break;
+  }
   // If this is a function argument, look in the argument attributes.
   if (auto blockArg = mlir::dyn_cast<mlir::BlockArgument>(value)) {
     if (blockArg.getOwner() && blockArg.getOwner()->isEntryBlock())
diff --git a/flang/test/Fir/assert-array-codegen.fir b/flang/test/Fir/assert-array-codegen.fir
new file mode 100644
index 00000000000000..4824a77f23940e
--- /dev/null
+++ b/flang/test/Fir/assert-array-codegen.fir
@@ -0,0 +1,75 @@
+// RUN: fir-opt --split-input-file --fir-to-llvm-ir %s | FileCheck %s
+// RUN: fir-opt --split-input-file --fir-to-llvm-ir="datalayout=e-p:64:64-p5:32:32-A5" %s | FileCheck %s --check-prefix=PRIVATE
+// RUN: fir-opt --split-input-file --fir-to-llvm-ir="target=i386-unknown-linux-gnu datalayout=e-p:32:32" %s | FileCheck %s --check-prefix=COUNT32
+
+// Five values select the array entry. Conversion, allocation and stores must
+// stay on the failure path, and the guarded result survives on success.
+func.func @array_values(%ok: i1, %a: i8, %b: i16, %c: i32, %d: index, %e: i64, %base: !fir.ref<i32>) -> !fir.ref<i32> {
+  %g = fir.assert %ok, bounds, "dimension %lld, position %lld, index %lld, bounds %lld:%lld (100%%)" values(%a, %b, %c, %d, %e : i8, i16, i32, index, i64) guard(%base : !fir.ref<i32>) loc("bounds.f90":17:2)
+  return %g : !fir.ref<i32>
+}
+// CHECK: llvm.func @_FortranAReportFatalUserErrorValueArray(!llvm.ptr, !llvm.ptr, i32, i64, !llvm.ptr) attributes {noreturn}
+// CHECK-LABEL: llvm.func @array_values(
+// CHECK-SAME: %[[OK:.*]]: i1, %[[A:.*]]: i8, %[[B:.*]]: i16, %[[C:.*]]: i32, %[[D:.*]]: i64, %[[E:.*]]: i64, %[[BASE:.*]]: !llvm.ptr)
+// CHECK-NEXT: llvm.cond_br %[[OK]] weights([2000, 1]), ^[[CONT:bb[0-9]+]], ^[[FAIL:bb[0-9]+]]
+// CHECK: ^[[CONT]]:
+// CHECK-NEXT: llvm.return %[[BASE]] : !llvm.ptr
+// CHECK: ^[[FAIL]]:
+// CHECK: %[[COUNT:.*]] = llvm.mlir.constant(5 : i64)
+// CHECK-NEXT: %[[STORAGE:.*]] = llvm.alloca %[[COUNT]] x i64 : (i64) -> !llvm.ptr
+// CHECK: %[[P0:.*]] = llvm.getelementptr %[[STORAGE]][%{{.*}}]
+// CHECK: %[[V0:.*]] = llvm.sext %[[A]] : i8 to i64
+// CHECK-NEXT: llvm.store %[[V0]], %[[P0]]
+// CHECK: %[[P1:.*]] = llvm.getelementptr %[[STORAGE]][%{{.*}}]
+// CHECK: %[[V1:.*]] = llvm.sext %[[B]] : i16 to i64
+// CHECK-NEXT: llvm.store %[[V1]], %[[P1]]
+// CHECK: %[[P2:.*]] = llvm.getelementptr %[[STORAGE]][%{{.*}}]
+// CHECK: %[[V2:.*]] = llvm.sext %[[C]] : i32 to i64
+// CHECK-NEXT: llvm.store %[[V2]], %[[P2]]
+// CHECK: %[[P3:.*]] = llvm.getelementptr %[[STORAGE]][%{{.*}}]
+// CHECK-NEXT: llvm.store %[[D]], %[[P3]]
+// CHECK: %[[P4:.*]] = llvm.getelementptr %[[STORAGE]][%{{.*}}]
+// CHECK-NEXT: llvm.store %[[E]], %[[P4]]
+// CHECK: llvm.call @_FortranAReportFatalUserErrorValueArray(%{{.*}}, %{{.*}}, %{{.*}}, %[[COUNT]], %[[STORAGE]])
+// CHECK-NEXT: llvm.unreachable
+// PRIVATE-LABEL: llvm.func @array_values
+// PRIVATE: %[[STORAGE:.*]] = llvm.alloca %{{.*}} x i64 : (i64) -> !llvm.ptr<5>
+// PRIVATE: %[[GENERIC:.*]] = llvm.addrspacecast %[[STORAGE]] : !llvm.ptr<5> to !llvm.ptr
+// PRIVATE-NEXT: llvm.call @_FortranAReportFatalUserErrorValueArray(%{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}, %[[GENERIC]])
+// COUNT32: llvm.func @_FortranAReportFatalUserErrorValueArray(!llvm.ptr, !llvm.ptr, i32, i32, !llvm.ptr) attributes {noreturn}
+// COUNT32-LABEL: llvm.func @array_values
+// COUNT32: %[[COUNT:.*]] = llvm.mlir.constant(5 : i32)
+// COUNT32-NEXT: llvm.alloca %[[COUNT]] x i64 : (i32) -> !llvm.ptr
+// COUNT32: llvm.call @_FortranAReportFatalUserErrorValueArray
+
+// -----
+
+// A complete pair of rank-six shapes needs twelve values. The device runtime
+// declaration and diagnostic strings must stay inside the GPU module.
+gpu.module @device {
+  gpu.func @shapes(%ok: i1, %n: index) {
+    fir.assert %ok, conformance, "shapes (%lld,%lld,%lld,%lld,%lld,%lld) and (%lld,%lld,%lld,%lld,%lld,%lld)" values(%n, %n, %n, %n, %n, %n, %n, %n, %n, %n, %n, %n : index, index, index, index, index, index, index, index, index, index, index, index) loc("shapes.f90":23:1)
+    gpu.return
+  }
+}
+// CHECK-LABEL: gpu.module @device {
+// CHECK-DAG: llvm.mlir.global internal constant @{{.*}}("shapes.f90\00")
+// CHECK-DAG: llvm.func @_FortranAReportFatalUserErrorValueArray({{.*}}) attributes {noreturn}
+// CHECK: gpu.func @shapes
+// CHECK: llvm.cond_br
+// CHECK: %[[COUNT:.*]] = llvm.mlir.constant(12 : i64)
+// CHECK: %[[ARRAY:.*]] = llvm.alloca %[[COUNT]] x i64
+// CHECK-COUNT-12: llvm.store
+// CHECK: llvm.call @_FortranAReportFatalUserErrorValueArray(%{{.*}}, %{{.*}}, %{{.*}}, %[[COUNT]], %[[ARRAY]])
+// CHECK-NEXT: llvm.unreachable
+
+// -----
+
+func.func private @_FortranAReportFatalUserErrorValueArray(!fir.ref<i8>, !fir.ref<i8>, i32, index, !fir.ref<i64>)
+func.func @existing_array(%ok: i1, %n: i64) {
+  fir.assert %ok, bounds, "%lld %lld %lld %lld %lld" values(%n, %n, %n, %n, %n : i64, i64, i64, i64, i64)
+  return
+}
+// CHECK: llvm.func @_FortranAReportFatalUserErrorValueArray({{.*}}) attributes {noreturn{{.*}}}
+// CHECK-LABEL: llvm.func @existing_array
+// CHECK: llvm.call @_FortranAReportFatalUserErrorValueArray
diff --git a/flang/test/Fir/assert-codegen.fir b/flang/test/Fir/assert-codegen.fir
new file mode 100644
index 00000000000000..7bd2126a27632d
--- /dev/null
+++ b/flang/test/Fir/assert-codegen.fir
@@ -0,0 +1,68 @@
+// RUN: fir-opt --split-input-file --fir-to-llvm-ir %s | FileCheck %s
+
+func.func @check(%ok: i1, %small: i32, %n: index, %array: !fir.ref<!fir.array<?xi32>>) -> !fir.ref<!fir.array<?xi32>> {
+  %g = fir.assert %ok, conformance, "extents %lld and %lld" values(%small, %n : i32, index) guard(%array : !fir.ref<!fir.array<?xi32>>) loc("source.f90":42:7)
+  return %g : !fir.ref<!fir.array<?xi32>>
+}
+// CHECK-DAG: llvm.mlir.global internal constant @{{.*}}("extents %lld and %lld\00")
+// CHECK-DAG: llvm.mlir.global internal constant @{{.*}}("source.f90\00")
+// CHECK-DAG: llvm.func @_FortranAReportFatalUserErrorValues(!llvm.ptr, !llvm.ptr, i32, i64, i64, i64, i64) attributes {noreturn}
+// CHECK-LABEL: llvm.func @check(
+// CHECK-SAME: %[[OK:.*]]: i1, %[[SMALL:.*]]: i32, %[[N:.*]]: i64, %[[ARRAY:.*]]: !llvm.ptr)
+// CHECK-NEXT: llvm.cond_br %[[OK]] weights([2000, 1]), ^[[CONT:bb[0-9]+]], ^[[FAIL:bb[0-9]+]]
+// CHECK: ^[[CONT]]:
+// CHECK-NEXT: llvm.return %[[ARRAY]] : !llvm.ptr
+// CHECK: ^[[FAIL]]:
+// CHECK: %[[MSG:.*]] = llvm.mlir.addressof
+// CHECK: %[[FILE:.*]] = llvm.mlir.addressof
+// CHECK: %[[LINE:.*]] = llvm.mlir.constant(42 : i32)
+// CHECK: %[[EXT:.*]] = llvm.sext %[[SMALL]] : i32 to i64
+// CHECK: %[[ZERO:.*]] = llvm.mlir.constant(0 : i64)
+// CHECK: llvm.call @_FortranAReportFatalUserErrorValues(%[[MSG]], %[[FILE]], %[[LINE]], %[[EXT]], %[[N]], %[[ZERO]], %{{.*}})
+// CHECK-NEXT: llvm.unreachable
+
+// -----
+
+// Existing declarations must also acquire noreturn.
+llvm.func @_FortranAReportFatalUserErrorValues(!llvm.ptr, !llvm.ptr, i32, i64, i64, i64, i64)
+func.func @existing_llvm(%ok: i1) {
+  fir.assert %ok, status, "allocation failed" loc(unknown)
+  return
+}
+// CHECK: llvm.func @_FortranAReportFatalUserErrorValues({{.*}}) attributes {noreturn}
+// CHECK-LABEL: llvm.func @existing_llvm
+// CHECK: llvm.cond_br
+// CHECK: %[[NULL:.*]] = llvm.mlir.zero : !llvm.ptr
+// CHECK: %[[LINE:.*]] = llvm.mlir.constant(0 : i32)
+// CHECK: llvm.call @_FortranAReportFatalUserErrorValues(%{{.*}}, %[[NULL]], %[[LINE]],
+// CHECK-NEXT: llvm.unreachable
+
+// -----
+
+func.func @existing_fir(%ok: i1) {
+  fir.assert %ok, status, "allocation failed" loc(fused["inlined"(callsite("inner.f90":9:2 at "outer.f90":80:1))])
+  return
+}
+func.func private @_FortranAReportFatalUserErrorValues(!fir.ref<i8>, !fir.ref<i8>, i32, i64, i64, i64, i64)
+// CHECK: llvm.mlir.global internal constant @{{.*}}("inner.f90\00")
+// CHECK-LABEL: llvm.func @existing_fir
+// CHECK: llvm.mlir.constant(9 : i32)
+// CHECK: llvm.call @_FortranAReportFatalUserErrorValues
+// CHECK: llvm.func @_FortranAReportFatalUserErrorValues({{.*}}) attributes {noreturn{{.*}}}
+
+// -----
+
+gpu.module @device {
+  gpu.func @check_device(%ok: i1, %n: i64) {
+    fir.assert %ok, bounds, "subscript %lld" values(%n : i64) loc("kernel.f90":11:3)
+    gpu.return
+  }
+}
+// CHECK-LABEL: gpu.module @device {
+// CHECK-DAG: llvm.mlir.global internal constant @{{.*}}("kernel.f90\00")
+// CHECK-DAG: llvm.mlir.global internal constant @{{.*}}("subscript %lld\00")
+// CHECK-DAG: llvm.func @_FortranAReportFatalUserErrorValues({{.*}}) attributes {noreturn}
+// CHECK: gpu.func @check_device
+// CHECK: llvm.cond_br
+// CHECK: llvm.call @_FortranAReportFatalUserErrorValues
+// CHECK: llvm.unreachable
diff --git a/flang/test/Fir/assert-invalid.fir b/flang/test/Fir/assert-invalid.fir
new file mode 100644
index 00000000000000..1003021e3733c0
--- /dev/null
+++ b/flang/test/Fir/assert-invalid.fir
@@ -0,0 +1,71 @@
+// RUN: fir-opt --split-input-file --verify-diagnostics %s
+
+func.func @format(%ok: i1, %i: i32) {
+  // expected-error at +1 {{message supports only %lld and %% format directives}}
+  fir.assert %ok, argument, "%d" values(%i : i32)
+  return
+}
+
+// -----
+
+func.func @trailing_percent(%ok: i1) {
+  // expected-error at +1 {{message supports only %lld and %% format directives}}
+  fir.assert %ok, argument, "bad %"
+  return
+}
+
+// -----
+
+func.func @too_few_values(%ok: i1) {
+  // expected-error at +1 {{number of %lld directives must match diagnostic values}}
+  fir.assert %ok, argument, "%lld"
+  return
+}
+
+// -----
+
+func.func @too_many_values(%ok: i1, %i: i32) {
+  // expected-error at +1 {{number of %lld directives must match diagnostic values}}
+  fir.assert %ok, argument, "%%lld" values(%i : i32)
+  return
+}
+
+// -----
+
+func.func @wide_integer(%ok: i1, %i: i128) {
+  // expected-error at +1 {{diagnostic integers must be at most 64 bits wide}}
+  fir.assert %ok, argument, "%lld" values(%i : i128)
+  return
+}
+
+// -----
+
+func.func @null_character(%ok: i1) {
+  // expected-error at +1 {{message must not contain an embedded null character}}
+  fir.assert %ok, argument, "bad\00argument"
+  return
+}
+
+// -----
+
+func.func @wrong_result_type(%ok: i1, %i: i32) {
+  // expected-error at +1 {{failed to verify that result types match guarded operand types}}
+  %g = "fir.assert"(%ok, %i) <{kind = #fir.runtime_check_kind<argument>, message = "bad", operandSegmentSizes = array<i32: 1, 0, 1>}> : (i1, i32) -> i64
+  return
+}
+
+// -----
+
+func.func @wrong_result_count(%ok: i1, %i: i32) {
+  // expected-error at +1 {{failed to verify that result types match guarded operand types}}
+  "fir.assert"(%ok, %i) <{kind = #fir.runtime_check_kind<argument>, message = "bad", operandSegmentSizes = array<i32: 1, 0, 1>}> : (i1, i32) -> ()
+  return
+}
+
+// -----
+
+func.func @real_value(%ok: i1, %x: f32) {
+  // expected-error at +1 {{must be variadic of signless integer or index}}
+  fir.assert %ok, argument, "%lld" values(%x : f32)
+  return
+}
diff --git a/flang/test/Fir/assert-memory.fir b/flang/test/Fir/assert-memory.fir
new file mode 100644
index 00000000000000..d28d49485f43dd
--- /dev/null
+++ b/flang/test/Fir/assert-memory.fir
@@ -0,0 +1,45 @@
+// RUN: fir-opt %s --cse --split-input-file | FileCheck %s --check-prefix=CSE
+// RUN: fir-opt %s --fir-memref-dataflow-opt --split-input-file | FileCheck %s --check-prefix=FORWARD
+
+// Checks touch no user memory, so they do not prevent load CSE.
+// CSE-LABEL: func.func @load_cse(
+// CSE: %[[V:.*]] = fir.load
+// CSE-NEXT: fir.assert
+// CSE-NOT: fir.load
+// CSE: arith.addi %[[V]], %[[V]]
+func.func @load_cse(%addr: !fir.ref<i32>, %ok: i1) -> i32 {
+  %a = fir.load %addr : !fir.ref<i32>
+  fir.assert %ok, argument, "invalid argument"
+  %b = fir.load %addr : !fir.ref<i32>
+  %sum = arith.addi %a, %b : i32
+  return %sum : i32
+}
+
+// -----
+
+// FORWARD-LABEL: func.func @store_forward(
+// FORWARD-SAME: %[[ADDR:.*]]: !fir.ref<i32>, %[[VALUE:.*]]: i32,
+// FORWARD: fir.store %[[VALUE]]
+// FORWARD-NEXT: fir.assert
+// FORWARD-NOT: fir.load
+// FORWARD: return %[[VALUE]]
+func.func @store_forward(%addr: !fir.ref<i32>, %value: i32, %ok: i1) -> i32 {
+  fir.store %value to %addr : !fir.ref<i32>
+  fir.assert %ok, argument, "invalid argument"
+  %load = fir.load %addr : !fir.ref<i32>
+  return %load : i32
+}
+
+// -----
+
+// CSE-LABEL: func.func @guarded_load(
+// CSE: fir.load
+// CSE: %[[G:.*]] = fir.assert
+// CSE-NEXT: %{{.*}} = fir.load %[[G]]
+func.func @guarded_load(%addr: !fir.ref<i32>, %ok: i1) -> i32 {
+  %a = fir.load %addr : !fir.ref<i32>
+  %guarded = fir.assert %ok, pointer, "invalid pointer" guard(%addr : !fir.ref<i32>)
+  %b = fir.load %guarded : !fir.ref<i32>
+  %sum = arith.addi %a, %b : i32
+  return %sum : i32
+}
diff --git a/flang/test/Fir/assert-no-fold-through.fir b/flang/test/Fir/assert-no-fold-through.fir
new file mode 100644
index 00000000000000..a783272d255a96
--- /dev/null
+++ b/flang/test/Fir/assert-no-fold-through.fir
@@ -0,0 +1,16 @@
+// RUN: fir-opt %s --canonicalize | FileCheck %s
+
+// Reboxing a guarded box address must not fold back to the original box.
+// CHECK-LABEL: func.func @guarded_box_address(
+// CHECK: %[[ADDR:.*]] = fir.box_addr
+// CHECK: %[[GUARDED:.*]] = fir.assert {{.*}} guard(%[[ADDR]]
+// CHECK: %[[BOX:.*]] = fir.embox %[[GUARDED]]
+// CHECK: return %[[BOX]]
+func.func @guarded_box_address(%box: !fir.box<!fir.array<10xi32>>, %ok: i1) -> !fir.box<!fir.array<10xi32>> {
+  %c10 = arith.constant 10 : index
+  %shape = fir.shape %c10 : (index) -> !fir.shape<1>
+  %addr = fir.box_addr %box : (!fir.box<!fir.array<10xi32>>) -> !fir.ref<!fir.array<10xi32>>
+  %g = fir.assert %ok, pointer, "invalid pointer" guard(%addr : !fir.ref<!fir.array<10xi32>>)
+  %result = fir.embox %g(%shape) : (!fir.ref<!fir.array<10xi32>>, !fir.shape<1>) -> !fir.box<!fir.array<10xi32>>
+  return %result : !fir.box<!fir.array<10xi32>>
+}
diff --git a/flang/test/Fir/assert.fir b/flang/test/Fir/assert.fir
new file mode 100644
index 00000000000000..e4c0bf7e65d909
--- /dev/null
+++ b/flang/test/Fir/assert.fir
@@ -0,0 +1,72 @@
+// RUN: fir-opt %s | fir-opt | FileCheck %s --check-prefix=ROUNDTRIP
+// RUN: fir-opt --canonicalize %s | FileCheck %s --check-prefix=FOLD
+
+func.func @asserts(%ok: i1, %n: index, %a: !fir.box<!fir.array<?xf32>>, %i: i32) -> (!fir.box<!fir.array<?xf32>>, i32) {
+  fir.assert %ok, status, "allocation failed"
+  %g:2 = fir.assert %ok, conformance, "extent %lld, expected %lld (100%%)" values(%n, %i : index, i32) guard(%a, %i : !fir.box<!fir.array<?xf32>>, i32)
+  fir.assert %ok, argument, "argument"
+  fir.assert %ok, bounds, "bounds"
+  fir.assert %ok, pointer, "pointer"
+  fir.assert %ok, do, "do"
+  fir.assert %ok, bits, "bits"
+  fir.assert %ok, divide, "divide"
+  return %g#0, %g#1 : !fir.box<!fir.array<?xf32>>, i32
+}
+// ROUNDTRIP-LABEL: func.func @asserts
+// ROUNDTRIP: fir.assert %{{.*}}, status, "allocation failed"
+// ROUNDTRIP: %[[G:.*]]:2 = fir.assert %{{.*}}, conformance, "extent %lld, expected %lld (100%%)" values(%{{.*}}, %{{.*}} : index, i32) guard(%{{.*}}, %{{.*}} : !fir.box<!fir.array<?xf32>>, i32)
+// ROUNDTRIP: fir.assert %{{.*}}, argument,
+// ROUNDTRIP: fir.assert %{{.*}}, bounds,
+// ROUNDTRIP: fir.assert %{{.*}}, pointer,
+// ROUNDTRIP: fir.assert %{{.*}}, do,
+// ROUNDTRIP: fir.assert %{{.*}}, bits,
+// ROUNDTRIP: fir.assert %{{.*}}, divide,
+// ROUNDTRIP: return %[[G]]#0, %[[G]]#1
+
+func.func @many_values(%ok: i1, %i: i32) {
+  fir.assert %ok, argument, "%lld %lld %lld %lld %lld" values(%i, %i, %i, %i, %i : i32, i32, i32, i32, i32)
+  return
+}
+// ROUNDTRIP-LABEL: func.func @many_values
+// ROUNDTRIP: fir.assert %{{.*}}, argument, "%lld %lld %lld %lld %lld" values(
+
+func.func @true_check(%x: i32) -> i32 {
+  %true = arith.constant true
+  fir.assert %true, status, "no results"
+  %g = fir.assert %true, argument, "guard" guard(%x : i32)
+  return %g : i32
+}
+// FOLD-LABEL: func.func @true_check(
+// FOLD-SAME: %[[X:.*]]: i32)
+// FOLD-NOT: fir.assert
+// FOLD: return %[[X]] : i32
+
+func.func @false_check(%x: i32) -> i32 {
+  %false = arith.constant false
+  fir.assert %false, status, "no results"
+  %g = fir.assert %false, argument, "guard" guard(%x : i32)
+  return %g : i32
+}
+// FOLD-LABEL: func.func @false_check
+// FOLD: fir.assert %{{.*}}, status, "no results"
+// FOLD: %[[G:.*]] = fir.assert %{{.*}}, argument, "guard"
+// FOLD: return %[[G]] : i32
+
+// Unused guards must not make a potentially failing assertion trivially dead.
+func.func @unused_results(%ok: i1, %x: i32) {
+  %g = fir.assert %ok, bounds, "unused" guard(%x : i32)
+  return
+}
+// FOLD-LABEL: func.func @unused_results
+// FOLD: fir.assert %{{.*}}, bounds, "unused" guard
+
+// This is not a view: folding a reboxed address would bypass the check.
+func.func @keep_guard(%ok: i1, %box: !fir.box<i32>) -> !fir.box<i32> {
+  %addr = fir.box_addr %box : (!fir.box<i32>) -> !fir.ref<i32>
+  %guard = fir.assert %ok, pointer, "null pointer" guard(%addr : !fir.ref<i32>)
+  %result = fir.embox %guard : (!fir.ref<i32>) -> !fir.box<i32>
+  return %result : !fir.box<i32>
+}
+// FOLD-LABEL: func.func @keep_guard
+// FOLD: %[[G:.*]] = fir.assert
+// FOLD: fir.embox %[[G]]
diff --git a/flang/unittests/Optimizer/FortranVariableTest.cpp b/flang/unittests/Optimizer/FortranVariableTest.cpp
index 7b363590cdea18..d2ed5547357622 100644
--- a/flang/unittests/Optimizer/FortranVariableTest.cpp
+++ b/flang/unittests/Optimizer/FortranVariableTest.cpp
@@ -8,6 +8,7 @@
 
 #include "gtest/gtest.h"
 #include "flang/Optimizer/Dialect/FIROps.h"
+#include "flang/Optimizer/Dialect/FIROpsSupport.h"
 #include "flang/Optimizer/Support/InitFIR.h"
 
 struct FortranVariableTest : public testing::Test {
@@ -160,3 +161,68 @@ TEST_F(FortranVariableTest, CharacterArray) {
   EXPECT_EQ(fortranVariable.getBase().getType(), addr.getType());
   EXPECT_EQ(fortranVariable.getExplicitCharLen(), len);
 }
+
+TEST_F(FortranVariableTest, AttributesThroughAssert) {
+  mlir::Location loc = getLoc();
+  auto target = fir::AllocaOp::create(*builder, loc, builder->getI32Type());
+  target->setAttr(fir::getTargetAttrName(), builder->getUnitAttr());
+  auto plain = fir::AllocaOp::create(*builder, loc, builder->getI32Type());
+  auto condition = mlir::arith::ConstantOp::create(
+      *builder, loc, builder->getBoolAttr(false));
+  auto guard = fir::AssertOp::create(*builder, loc, condition,
+      fir::RuntimeCheckKind::conformance, "mismatch", mlir::ValueRange{},
+      mlir::ValueRange{plain, target});
+  auto nested = fir::AssertOp::create(*builder, loc, condition,
+      fir::RuntimeCheckKind::pointer, "invalid pointer", mlir::ValueRange{},
+      guard.getResults());
+  EXPECT_FALSE(
+      fir::valueHasFirAttribute(nested.getResult(0), fir::getTargetAttrName()));
+  EXPECT_TRUE(
+      fir::valueHasFirAttribute(nested.getResult(1), fir::getTargetAttrName()));
+  EXPECT_FALSE(fir::valueMayHaveFirAttributes(
+      nested.getResult(0), {fir::getTargetAttrName()}));
+  EXPECT_TRUE(fir::valueMayHaveFirAttributes(
+      nested.getResult(1), {fir::getTargetAttrName()}));
+}
+
+TEST_F(FortranVariableTest, BoxAttributesThroughAssertAndLoad) {
+  mlir::Location loc = getLoc();
+  auto boxTy = fir::BoxType::get(builder->getF32Type());
+  auto boxAddr = fir::AllocaOp::create(*builder, loc, boxTy);
+  boxAddr->setAttr(fir::getTargetAttrName(), builder->getUnitAttr());
+  auto condition = mlir::arith::ConstantOp::create(
+      *builder, loc, builder->getBoolAttr(false));
+  auto guardAddr = fir::AssertOp::create(*builder, loc, condition,
+      fir::RuntimeCheckKind::pointer, "invalid pointer", mlir::ValueRange{},
+      mlir::ValueRange{boxAddr});
+  auto box = fir::LoadOp::create(*builder, loc, guardAddr.getResult(0));
+  auto guardBox = fir::AssertOp::create(*builder, loc, condition,
+      fir::RuntimeCheckKind::conformance, "mismatch", mlir::ValueRange{},
+      mlir::ValueRange{box});
+  EXPECT_TRUE(fir::valueHasFirAttribute(
+      guardBox.getResult(0), fir::getTargetAttrName()));
+  EXPECT_FALSE(fir::mayBeAbsentBox(guardBox.getResult(0)));
+}
+
+TEST_F(FortranVariableTest, OptionalBoxThroughAssert) {
+  mlir::Location loc = getLoc();
+  auto boxTy = fir::BoxType::get(builder->getF32Type());
+  mlir::Value box = builder->getInsertionBlock()->addArgument(boxTy, loc);
+  auto condition = mlir::arith::ConstantOp::create(
+      *builder, loc, builder->getBoolAttr(false));
+  for (bool optional : {false, true}) {
+    auto flags = optional ? fir::FortranVariableFlagsEnum::optional
+                          : fir::FortranVariableFlagsEnum::None;
+    auto declare = fir::DeclareOp::create(*builder, loc, boxTy, box,
+        /*shape=*/mlir::Value{}, /*typeParams=*/mlir::ValueRange{},
+        /*dummy_scope=*/nullptr, /*storage=*/nullptr, /*storage_offset=*/0,
+        builder->getStringAttr("x"),
+        fir::FortranVariableFlagsAttr::get(&context, flags),
+        /*data_attr=*/cuf::DataAttributeAttr{},
+        /*dummy_arg_no=*/mlir::IntegerAttr{});
+    auto guard = fir::AssertOp::create(*builder, loc, condition,
+        fir::RuntimeCheckKind::conformance, "mismatch", mlir::ValueRange{},
+        mlir::ValueRange{declare});
+    EXPECT_EQ(fir::mayBeAbsentBox(guard.getResult(0)), optional);
+  }
+}

>From cc227eb2e02413326f487433ae43407f8d24a327 Mon Sep 17 00:00:00 2001
From: Jean Perier <jperier at nvidia.com>
Date: Tue, 13 Oct 2026 01:54:25 -0700
Subject: [PATCH 2/2] [flang] Refine fir.assert verification and target
 lowering

Restrict diagnostic values to i8, i16, i32, i64, and index, requiring
producers to choose how boolean values are extended. Use target global
address spaces for diagnostic strings and cast their addresses for calls.

Use runtime entry-name macros and a named fixed-value limit, remove a
redundant verifier check, and cover incompatible declarations, symbol
conflicts, explicit boolean extension, and global address spaces in tests.
---
 flang/docs/InlineRuntimeChecks.md             |  8 +-
 .../include/flang/Optimizer/Dialect/FIROps.td |  7 +-
 flang/lib/Optimizer/CodeGen/CodeGen.cpp       | 21 +++--
 flang/lib/Optimizer/Dialect/FIROps.cpp        |  7 --
 flang/test/Fir/assert-array-codegen.fir       | 14 ++++
 flang/test/Fir/assert-codegen-invalid.fir     | 79 +++++++++++++++++++
 flang/test/Fir/assert-codegen.fir             | 29 +++++++
 flang/test/Fir/assert-invalid.fir             | 20 ++++-
 8 files changed, 164 insertions(+), 21 deletions(-)
 create mode 100644 flang/test/Fir/assert-codegen-invalid.fir

diff --git a/flang/docs/InlineRuntimeChecks.md b/flang/docs/InlineRuntimeChecks.md
index 1d1d77bf924f45..7ef61dd473de59 100644
--- a/flang/docs/InlineRuntimeChecks.md
+++ b/flang/docs/InlineRuntimeChecks.md
@@ -104,9 +104,11 @@ Non-goals:
 - `%ok` (`i1`) is the condition that must hold.
 - `<kind>` is a check category (section 4). Options act on categories.
 - `<message>` is the error text, a `printf` format.
-- `values(%v0, ... : T0, ...)` are optional integer or `index` SSA values
-  (`T0`, ... are their types) printed into the message, one per `%lld`
-  directive. They are only read on the failure path. For example, the two
+- `values(%v0, ... : T0, ...)` are optional `i8`, `i16`, `i32`, `i64`, or
+  `index` SSA values (`T0`, ... are their types) printed as signed integers
+  into the message, one per `%lld` directive. Boolean diagnostics must be
+  explicitly extended by the producer to select their signed or unsigned
+  interpretation. Values are only read on the failure path. For example, the two
   extents in `"DOT_PRODUCT: SIZE(VECTOR_A) is %lld but SIZE(VECTOR_B) is %lld"`.
 - `guard(%x0, ... : U0, ...)` are the optional guarded values, of any type.
   The op has one result per guarded value, with the same type (`%g#i` has
diff --git a/flang/include/flang/Optimizer/Dialect/FIROps.td b/flang/include/flang/Optimizer/Dialect/FIROps.td
index 5a06a570f9ef4c..fc00daac1695b1 100644
--- a/flang/include/flang/Optimizer/Dialect/FIROps.td
+++ b/flang/include/flang/Optimizer/Dialect/FIROps.td
@@ -93,7 +93,9 @@ def fir_AssertOp : fir_Op<"assert", [
     reporting `message` at the operation's source location. The message may
     contain `%lld` directives, one for each integer or index operand
     in `values`, and `%%` for a literal percent sign. Diagnostic values are
-    interpreted as signed integers and must fit in 64 bits.
+    interpreted as signed integers and must have type `i8`, `i16`, `i32`,
+    `i64`, or `index`. Boolean values must be explicitly extended to one of
+    these types, choosing signed or unsigned interpretation at the producer.
 
     Consumers of the results depend on the check. The operation is deliberately
     not speculatable or a view: looking through it in a rewrite would discard
@@ -108,7 +110,8 @@ def fir_AssertOp : fir_Op<"assert", [
   }];
   let arguments = (ins I1:$condition, fir_RuntimeCheckKindAttr:$kind,
                        StrAttr:$message,
-                       Variadic<AnySignlessIntegerOrIndex>:$values,
+                       Variadic<AnyTypeOf<[I8, I16, I32, I64, Index],
+                           "8-, 16-, 32-, or 64-bit signless integer or index">>:$values,
                        Variadic<AnyType>:$guarded);
   let results = (outs Variadic<AnyType>:$results);
   let assemblyFormat = [{
diff --git a/flang/lib/Optimizer/CodeGen/CodeGen.cpp b/flang/lib/Optimizer/CodeGen/CodeGen.cpp
index 161e7e2ed1254e..79605d0c6eaca7 100644
--- a/flang/lib/Optimizer/CodeGen/CodeGen.cpp
+++ b/flang/lib/Optimizer/CodeGen/CodeGen.cpp
@@ -32,6 +32,7 @@
 #include "flang/Runtime/CUDA/memory.h"
 #include "flang/Runtime/allocator-registry-consts.h"
 #include "flang/Runtime/descriptor-consts.h"
+#include "flang/Runtime/entry-names.h"
 #include "flang/Semantics/runtime-type-info.h"
 #include "mlir/Conversion/ArithAttrToLLVM/AttrToLLVMConverter.h"
 #include "mlir/Conversion/ArithToLLVM/ArithToLLVM.h"
@@ -4436,6 +4437,8 @@ static mlir::FileLineColLoc findAssertSourceLocation(mlir::Location loc) {
 struct AssertOpConversion : public fir::FIROpConversion<fir::AssertOp> {
   using FIROpConversion::FIROpConversion;
 
+  static constexpr unsigned maxInlineValues = 4;
+
   llvm::LogicalResult
   matchAndRewrite(fir::AssertOp op, OpAdaptor adaptor,
                   mlir::ConversionPatternRewriter &rewriter) const override {
@@ -4446,18 +4449,18 @@ struct AssertOpConversion : public fir::FIROpConversion<fir::AssertOp> {
     auto ptrTy = getLlvmPtrType(op.getContext());
     auto i64Ty = rewriter.getI64Type();
     auto countTy = rewriter.getIntegerType(lowerTy().getPointerBitwidth(0));
-    bool useArray = adaptor.getValues().size() > 4;
+    bool useArray = adaptor.getValues().size() > maxInlineValues;
     llvm::SmallVector<mlir::Type> argumentTypes{ptrTy, ptrTy,
                                                 rewriter.getI32Type()};
     if (useArray)
       argumentTypes.append({countTy, ptrTy});
     else
-      argumentTypes.append(4, i64Ty);
+      argumentTypes.append(maxInlineValues, i64Ty);
     auto functionType = mlir::LLVM::LLVMFunctionType::get(
         mlir::LLVM::LLVMVoidType::get(op.getContext()), argumentTypes);
     llvm::StringRef runtimeName =
-        useArray ? "_FortranAReportFatalUserErrorValueArray"
-                 : "_FortranAReportFatalUserErrorValues";
+        useArray ? RTNAME_STRING(ReportFatalUserErrorValueArray)
+                 : RTNAME_STRING(ReportFatalUserErrorValues);
     mlir::Operation *declaration =
         mlir::SymbolTable::lookupSymbolIn(scope, runtimeName);
     if (declaration) {
@@ -4496,6 +4499,7 @@ struct AssertOpConversion : public fir::FIROpConversion<fir::AssertOp> {
     auto stringAddress = [&](llvm::StringRef text) -> mlir::Value {
       std::string terminated = text.str() + '\0';
       std::string name = fir::factory::uniqueCGIdent("assert", terminated);
+      unsigned globalAS = getGlobalAddressSpace(rewriter);
       if (!mlir::SymbolTable::lookupSymbolIn(scope, name)) {
         mlir::OpBuilder::InsertionGuard guard(rewriter);
         rewriter.setInsertionPointToStart(&scope->getRegion(0).front());
@@ -4503,9 +4507,12 @@ struct AssertOpConversion : public fir::FIROpConversion<fir::AssertOp> {
                                                       terminated.size());
         mlir::LLVM::GlobalOp::create(rewriter, loc, arrayTy, /*constant=*/true,
                                      mlir::LLVM::Linkage::Internal, name,
-                                     rewriter.getStringAttr(terminated));
+                                     rewriter.getStringAttr(terminated),
+                                     /*alignment=*/0, globalAS);
       }
-      return mlir::LLVM::AddressOfOp::create(rewriter, loc, ptrTy, name);
+      return replaceWithAddrOfOrASCast(rewriter, loc, globalAS,
+                                       getProgramAddressSpace(rewriter), name,
+                                       ptrTy);
     };
 
     mlir::Value message = stringAddress(op.getMessage());
@@ -4542,7 +4549,7 @@ struct AssertOpConversion : public fir::FIROpConversion<fir::AssertOp> {
     } else {
       for (mlir::Value value : adaptor.getValues())
         arguments.push_back(integerCast(loc, rewriter, i64Ty, value));
-      while (arguments.size() < 7)
+      while (arguments.size() < functionType.getNumParams())
         arguments.push_back(mlir::LLVM::ConstantOp::create(
             rewriter, loc, i64Ty, rewriter.getI64IntegerAttr(0)));
     }
diff --git a/flang/lib/Optimizer/Dialect/FIROps.cpp b/flang/lib/Optimizer/Dialect/FIROps.cpp
index 4f3ab9984f99e8..3d079afa0d1a51 100644
--- a/flang/lib/Optimizer/Dialect/FIROps.cpp
+++ b/flang/lib/Optimizer/Dialect/FIROps.cpp
@@ -61,13 +61,6 @@ bool fir::useStrictVolatileVerification() {
 //===----------------------------------------------------------------------===//
 
 llvm::LogicalResult fir::AssertOp::verify() {
-  if (getGuarded().getTypes() != getResultTypes())
-    return emitOpError("result types must match guarded operand types");
-  for (mlir::Type type : getValues().getTypes())
-    if (auto integer = mlir::dyn_cast<mlir::IntegerType>(type))
-      if (integer.getWidth() > 64)
-        return emitOpError("diagnostic integers must be at most 64 bits wide");
-
   llvm::StringRef message = getMessage();
   if (message.contains('\0'))
     return emitOpError("message must not contain an embedded null character");
diff --git a/flang/test/Fir/assert-array-codegen.fir b/flang/test/Fir/assert-array-codegen.fir
index 4824a77f23940e..10a842da41921b 100644
--- a/flang/test/Fir/assert-array-codegen.fir
+++ b/flang/test/Fir/assert-array-codegen.fir
@@ -1,6 +1,7 @@
 // RUN: fir-opt --split-input-file --fir-to-llvm-ir %s | FileCheck %s
 // RUN: fir-opt --split-input-file --fir-to-llvm-ir="datalayout=e-p:64:64-p5:32:32-A5" %s | FileCheck %s --check-prefix=PRIVATE
 // RUN: fir-opt --split-input-file --fir-to-llvm-ir="target=i386-unknown-linux-gnu datalayout=e-p:32:32" %s | FileCheck %s --check-prefix=COUNT32
+// RUN: fir-opt --split-input-file --fir-to-llvm-ir="target=amdgcn-amd-amdhsa datalayout=e-p:64:64-p1:64:64-p5:32:32-A5-G1" %s | FileCheck %s --check-prefix=GLOBAL
 
 // Five values select the array entry. Conversion, allocation and stores must
 // stay on the failure path, and the guarded result survives on success.
@@ -41,6 +42,14 @@ func.func @array_values(%ok: i1, %a: i8, %b: i16, %c: i32, %d: index, %e: i64, %
 // COUNT32: %[[COUNT:.*]] = llvm.mlir.constant(5 : i32)
 // COUNT32-NEXT: llvm.alloca %[[COUNT]] x i64 : (i32) -> !llvm.ptr
 // COUNT32: llvm.call @_FortranAReportFatalUserErrorValueArray
+// GLOBAL-DAG: llvm.mlir.global internal constant @{{.*}}("bounds.f90\00") {{.*}}addr_space = 1
+// GLOBAL-DAG: llvm.mlir.global internal constant @{{.*}}("dimension %lld, position %lld, index %lld, bounds %lld:%lld (100%%)\00") {{.*}}addr_space = 1
+// GLOBAL-LABEL: llvm.func @array_values
+// GLOBAL: %[[MSG:.*]] = llvm.addrspacecast %{{.*}} : !llvm.ptr<1> to !llvm.ptr
+// GLOBAL: %[[FILE:.*]] = llvm.addrspacecast %{{.*}} : !llvm.ptr<1> to !llvm.ptr
+// GLOBAL: %[[STORAGE:.*]] = llvm.alloca %{{.*}} x i64 : (i64) -> !llvm.ptr<5>
+// GLOBAL: %[[VALUES:.*]] = llvm.addrspacecast %[[STORAGE]] : !llvm.ptr<5> to !llvm.ptr
+// GLOBAL: llvm.call @_FortranAReportFatalUserErrorValueArray(%[[MSG]], %[[FILE]], %{{.*}}, %{{.*}}, %[[VALUES]])
 
 // -----
 
@@ -62,6 +71,11 @@ gpu.module @device {
 // CHECK-COUNT-12: llvm.store
 // CHECK: llvm.call @_FortranAReportFatalUserErrorValueArray(%{{.*}}, %{{.*}}, %{{.*}}, %[[COUNT]], %[[ARRAY]])
 // CHECK-NEXT: llvm.unreachable
+// GLOBAL-LABEL: gpu.module @device {
+// GLOBAL-DAG: llvm.mlir.global internal constant @{{.*}}("shapes.f90\00") {{.*}}addr_space = 1
+// GLOBAL: gpu.func @shapes
+// GLOBAL: llvm.addrspacecast %{{.*}} : !llvm.ptr<1> to !llvm.ptr
+// GLOBAL: llvm.call @_FortranAReportFatalUserErrorValueArray
 
 // -----
 
diff --git a/flang/test/Fir/assert-codegen-invalid.fir b/flang/test/Fir/assert-codegen-invalid.fir
new file mode 100644
index 00000000000000..1be4ee2ff9c5c0
--- /dev/null
+++ b/flang/test/Fir/assert-codegen-invalid.fir
@@ -0,0 +1,79 @@
+// RUN: fir-opt --split-input-file --fir-to-llvm-ir --verify-diagnostics %s
+
+llvm.func @_FortranAReportFatalUserErrorValues(!llvm.ptr, !llvm.ptr, i32, i32, i64, i64, i64)
+func.func @llvm_signature(%ok: i1) {
+  // expected-error at +2 {{incompatible runtime function declaration}}
+  // expected-error at +1 {{failed to legalize operation}}
+  fir.assert %ok, status, "x"
+  return
+}
+
+// -----
+
+fir.global @_FortranAReportFatalUserErrorValues : i32
+func.func @name_conflict(%ok: i1) {
+  // expected-error at +2 {{runtime function name conflicts with a symbol}}
+  // expected-error at +1 {{failed to legalize operation}}
+  fir.assert %ok, status, "x"
+  return
+}
+
+// -----
+
+func.func @func_arity(%ok: i1) {
+  // expected-error at +2 {{incompatible runtime function declaration}}
+  // expected-error at +1 {{failed to legalize operation}}
+  fir.assert %ok, status, "x"
+  return
+}
+func.func private @_FortranAReportFatalUserErrorValues(!fir.ref<i8>, !fir.ref<i8>, i32, i64, i64, i64)
+
+// -----
+
+func.func @func_argument_type(%ok: i1) {
+  // expected-error at +2 {{incompatible runtime function declaration}}
+  // expected-error at +1 {{failed to legalize operation}}
+  fir.assert %ok, status, "x"
+  return
+}
+func.func private @_FortranAReportFatalUserErrorValues(!fir.ref<i8>, !fir.ref<i8>, i32, i32, i64, i64, i64)
+
+// -----
+
+func.func @func_result(%ok: i1) {
+  // expected-error at +2 {{incompatible runtime function declaration}}
+  // expected-error at +1 {{failed to legalize operation}}
+  fir.assert %ok, status, "x"
+  return
+}
+func.func private @_FortranAReportFatalUserErrorValues(!fir.ref<i8>, !fir.ref<i8>, i32, i64, i64, i64, i64) -> i32
+
+// -----
+
+llvm.func @_FortranAReportFatalUserErrorValueArray(!llvm.ptr, !llvm.ptr, i32, i32, !llvm.ptr)
+func.func @array_llvm_signature(%ok: i1, %n: i64) {
+  // expected-error at +2 {{incompatible runtime function declaration}}
+  // expected-error at +1 {{failed to legalize operation}}
+  fir.assert %ok, bounds, "%lld %lld %lld %lld %lld" values(%n, %n, %n, %n, %n : i64, i64, i64, i64, i64)
+  return
+}
+
+// -----
+
+fir.global @_FortranAReportFatalUserErrorValueArray : i32
+func.func @array_name_conflict(%ok: i1, %n: i64) {
+  // expected-error at +2 {{runtime function name conflicts with a symbol}}
+  // expected-error at +1 {{failed to legalize operation}}
+  fir.assert %ok, bounds, "%lld %lld %lld %lld %lld" values(%n, %n, %n, %n, %n : i64, i64, i64, i64, i64)
+  return
+}
+
+// -----
+
+func.func @array_func_argument_type(%ok: i1, %n: i64) {
+  // expected-error at +2 {{incompatible runtime function declaration}}
+  // expected-error at +1 {{failed to legalize operation}}
+  fir.assert %ok, bounds, "%lld %lld %lld %lld %lld" values(%n, %n, %n, %n, %n : i64, i64, i64, i64, i64)
+  return
+}
+func.func private @_FortranAReportFatalUserErrorValueArray(!fir.ref<i8>, !fir.ref<i8>, i32, i64, i64)
diff --git a/flang/test/Fir/assert-codegen.fir b/flang/test/Fir/assert-codegen.fir
index 7bd2126a27632d..1cb10067b09dd4 100644
--- a/flang/test/Fir/assert-codegen.fir
+++ b/flang/test/Fir/assert-codegen.fir
@@ -1,4 +1,5 @@
 // RUN: fir-opt --split-input-file --fir-to-llvm-ir %s | FileCheck %s
+// RUN: fir-opt --split-input-file --fir-to-llvm-ir="target=amdgcn-amd-amdhsa datalayout=e-p:64:64-p1:64:64-p5:32:32-A5-G1" %s | FileCheck %s --check-prefix=GLOBAL
 
 func.func @check(%ok: i1, %small: i32, %n: index, %array: !fir.ref<!fir.array<?xi32>>) -> !fir.ref<!fir.array<?xi32>> {
   %g = fir.assert %ok, conformance, "extents %lld and %lld" values(%small, %n : i32, index) guard(%array : !fir.ref<!fir.array<?xi32>>) loc("source.f90":42:7)
@@ -20,6 +21,14 @@ func.func @check(%ok: i1, %small: i32, %n: index, %array: !fir.ref<!fir.array<?x
 // CHECK: %[[ZERO:.*]] = llvm.mlir.constant(0 : i64)
 // CHECK: llvm.call @_FortranAReportFatalUserErrorValues(%[[MSG]], %[[FILE]], %[[LINE]], %[[EXT]], %[[N]], %[[ZERO]], %{{.*}})
 // CHECK-NEXT: llvm.unreachable
+// GLOBAL-DAG: llvm.mlir.global internal constant @[[MSG:.*]]("extents %lld and %lld\00") {{.*}}addr_space = 1
+// GLOBAL-DAG: llvm.mlir.global internal constant @[[FILE:.*]]("source.f90\00") {{.*}}addr_space = 1
+// GLOBAL-LABEL: llvm.func @check(
+// GLOBAL: %[[MSGADDR:.*]] = llvm.mlir.addressof @[[MSG]] : !llvm.ptr<1>
+// GLOBAL-NEXT: %[[MSGCAST:.*]] = llvm.addrspacecast %[[MSGADDR]] : !llvm.ptr<1> to !llvm.ptr
+// GLOBAL-NEXT: %[[FILEADDR:.*]] = llvm.mlir.addressof @[[FILE]] : !llvm.ptr<1>
+// GLOBAL-NEXT: %[[FILECAST:.*]] = llvm.addrspacecast %[[FILEADDR]] : !llvm.ptr<1> to !llvm.ptr
+// GLOBAL: llvm.call @_FortranAReportFatalUserErrorValues(%[[MSGCAST]], %[[FILECAST]],
 
 // -----
 
@@ -66,3 +75,23 @@ gpu.module @device {
 // CHECK: llvm.cond_br
 // CHECK: llvm.call @_FortranAReportFatalUserErrorValues
 // CHECK: llvm.unreachable
+// GLOBAL-LABEL: gpu.module @device {
+// GLOBAL-DAG: llvm.mlir.global internal constant @{{.*}}("kernel.f90\00") {{.*}}addr_space = 1
+// GLOBAL-DAG: llvm.mlir.global internal constant @{{.*}}("subscript %lld\00") {{.*}}addr_space = 1
+// GLOBAL: gpu.func @check_device
+// GLOBAL: llvm.addrspacecast %{{.*}} : !llvm.ptr<1> to !llvm.ptr
+// GLOBAL: llvm.call @_FortranAReportFatalUserErrorValues
+
+// -----
+
+// Producers choose how a boolean diagnostic is interpreted.
+func.func @boolean_value(%ok: i1, %flag: i1) {
+  %value = arith.extui %flag : i1 to i8
+  fir.assert %ok, argument, "flag is %lld" values(%value : i8)
+  return
+}
+// CHECK-LABEL: llvm.func @boolean_value(
+// CHECK: %[[VALUE:.*]] = llvm.zext %{{.*}} : i1 to i8
+// CHECK: llvm.cond_br
+// CHECK: %[[EXT:.*]] = llvm.sext %[[VALUE]] : i8 to i64
+// CHECK: llvm.call @_FortranAReportFatalUserErrorValues(%{{.*}}, %{{.*}}, %{{.*}}, %[[EXT]],
diff --git a/flang/test/Fir/assert-invalid.fir b/flang/test/Fir/assert-invalid.fir
index 1003021e3733c0..20f6b6e52b7aca 100644
--- a/flang/test/Fir/assert-invalid.fir
+++ b/flang/test/Fir/assert-invalid.fir
@@ -33,7 +33,7 @@ func.func @too_many_values(%ok: i1, %i: i32) {
 // -----
 
 func.func @wide_integer(%ok: i1, %i: i128) {
-  // expected-error at +1 {{diagnostic integers must be at most 64 bits wide}}
+  // expected-error at +1 {{must be variadic of 8-, 16-, 32-, or 64-bit signless integer or index}}
   fir.assert %ok, argument, "%lld" values(%i : i128)
   return
 }
@@ -65,7 +65,23 @@ func.func @wrong_result_count(%ok: i1, %i: i32) {
 // -----
 
 func.func @real_value(%ok: i1, %x: f32) {
-  // expected-error at +1 {{must be variadic of signless integer or index}}
+  // expected-error at +1 {{must be variadic of 8-, 16-, 32-, or 64-bit signless integer or index}}
   fir.assert %ok, argument, "%lld" values(%x : f32)
   return
 }
+
+// -----
+
+func.func @boolean_value(%ok: i1, %flag: i1) {
+  // expected-error at +1 {{must be variadic of 8-, 16-, 32-, or 64-bit signless integer or index}}
+  fir.assert %ok, argument, "%lld" values(%flag : i1)
+  return
+}
+
+// -----
+
+func.func @nonstandard_integer_width(%ok: i1, %i: i24) {
+  // expected-error at +1 {{must be variadic of 8-, 16-, 32-, or 64-bit signless integer or index}}
+  fir.assert %ok, argument, "%lld" values(%i : i24)
+  return
+}



More information about the flang-commits mailing list