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

via flang-commits flang-commits at lists.llvm.org
Fri Oct 16 01:26:42 PDT 2026


Author: jeanPerier
Date: 2026-10-16T10:26:34+02:00
New Revision: 69ea540ba5d90d569cc7f53213b8bf0fcb7469d6

URL: https://github.com/llvm/llvm-project/commit/69ea540ba5d90d569cc7f53213b8bf0fcb7469d6
DIFF: https://github.com/llvm/llvm-project/commit/69ea540ba5d90d569cc7f53213b8bf0fcb7469d6.diff

LOG: [flang] Add fir.assert and its LLVM code generation (#231245)

Add the new fir.assert operation for which the design was introduced in
https://github.com/llvm/llvm-project/pull/228502

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.
    
For the LLVM code generation, generate 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.

Assisted-by: AI

Added: 
    flang/test/Fir/assert-array-codegen.fir
    flang/test/Fir/assert-codegen-invalid.fir
    flang/test/Fir/assert-codegen.fir
    flang/test/Fir/assert-invalid.fir
    flang/test/Fir/assert-memory.fir
    flang/test/Fir/assert-no-fold-through.fir
    flang/test/Fir/assert.fir

Modified: 
    flang/docs/InlineRuntimeChecks.md
    flang/include/flang/Optimizer/Dialect/FIRAttr.td
    flang/include/flang/Optimizer/Dialect/FIROps.h
    flang/include/flang/Optimizer/Dialect/FIROps.td
    flang/lib/Optimizer/CodeGen/CodeGen.cpp
    flang/lib/Optimizer/Dialect/FIROps.cpp
    flang/unittests/Optimizer/FortranVariableTest.cpp

Removed: 
    


################################################################################
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/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..fc00daac1695b1 100644
--- a/flang/include/flang/Optimizer/Dialect/FIROps.td
+++ b/flang/include/flang/Optimizer/Dialect/FIROps.td
@@ -81,6 +81,54 @@ 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 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
+    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<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 = [{
+    $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..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"
@@ -4415,6 +4416,156 @@ 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;
+
+  static constexpr unsigned maxInlineValues = 4;
+
+  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() > maxInlineValues;
+    llvm::SmallVector<mlir::Type> argumentTypes{ptrTy, ptrTy,
+                                                rewriter.getI32Type()};
+    if (useArray)
+      argumentTypes.append({countTy, ptrTy});
+    else
+      argumentTypes.append(maxInlineValues, i64Ty);
+    auto functionType = mlir::LLVM::LLVMFunctionType::get(
+        mlir::LLVM::LLVMVoidType::get(op.getContext()), argumentTypes);
+    llvm::StringRef runtimeName =
+        useArray ? RTNAME_STRING(ReportFatalUserErrorValueArray)
+                 : RTNAME_STRING(ReportFatalUserErrorValues);
+    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);
+      unsigned globalAS = getGlobalAddressSpace(rewriter);
+      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),
+                                     /*alignment=*/0, globalAS);
+      }
+      return replaceWithAddrOfOrASCast(rewriter, loc, globalAS,
+                                       getProgramAddressSpace(rewriter), name,
+                                       ptrTy);
+    };
+
+    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() < functionType.getNumParams())
+        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 +5344,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..3d079afa0d1a51 100644
--- a/flang/lib/Optimizer/Dialect/FIROps.cpp
+++ b/flang/lib/Optimizer/Dialect/FIROps.cpp
@@ -56,6 +56,52 @@ bool fir::useStrictVolatileVerification() {
   return clUseStrictVolatileVerification;
 }
 
+//===----------------------------------------------------------------------===//
+// AssertOp
+//===----------------------------------------------------------------------===//
+
+llvm::LogicalResult fir::AssertOp::verify() {
+  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 +293,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 +6157,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..10a842da41921b
--- /dev/null
+++ b/flang/test/Fir/assert-array-codegen.fir
@@ -0,0 +1,89 @@
+// 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.
+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
+// 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]])
+
+// -----
+
+// 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
+// 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
+
+// -----
+
+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-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
new file mode 100644
index 00000000000000..1cb10067b09dd4
--- /dev/null
+++ b/flang/test/Fir/assert-codegen.fir
@@ -0,0 +1,97 @@
+// 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)
+  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
+// 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]],
+
+// -----
+
+// 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
+// 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
new file mode 100644
index 00000000000000..20f6b6e52b7aca
--- /dev/null
+++ b/flang/test/Fir/assert-invalid.fir
@@ -0,0 +1,87 @@
+// 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 {{must be variadic of 8-, 16-, 32-, or 64-bit signless integer or index}}
+  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 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
+}

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);
+  }
+}


        


More information about the flang-commits mailing list