[flang-commits] [flang] a70234b - [flang] Propagate INTENT(IN) dummy arguments as readonly (#207732)

via flang-commits flang-commits at lists.llvm.org
Mon Jul 13 21:50:20 PDT 2026


Author: Sergey Shcherbinin
Date: 2026-07-14T10:20:15+05:30
New Revision: a70234b00d12aaa7723eb2ae5e8efe3348aeeb37

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

LOG: [flang] Propagate INTENT(IN) dummy arguments as readonly (#207732)

Mark eligible `INTENT(IN)` dummy data objects with a `fir.read_only`
argument attribute during lowering, and teach `FunctionAttr` to
translate the marker to LLVM `readonly` for reference arguments when
optimizing for speed.

LLVM FunctionAttrs can infer `readonly` later in the pipeline, but this
happens too late for IPSCCP function specialization.
`FunctionSpecializer::promoteConstantStackValues` requires
`onlyReadsMemory()` before promoting constant by-reference scalar
arguments. Providing `readonly` satisfies this check and allows these
arguments to be promoted, enabling function specialization, including in
non-LTO builds.

For direct-data arguments, restrict propagation to by-reference
non-character intrinsic scalars. Ordinary arrays and derived-type
arguments are excluded because compiler-generated copy-out may write
back through a forwarded dummy argument. LLVM `readonly` does not permit
such writes, even when the stored value is unchanged.

`INTENT(IN)` `POINTER` and `ALLOCATABLE` dummies are supported with
shallow descriptor semantics: `readonly` protects the descriptor
storage, but does not imply that data addressed by the descriptor is
read-only. In particular, a `POINTER` target may still be modified.

Keep `readonly` handling independent from `noalias` and `nocapture`.
`TARGET` arguments may receive `readonly`, but must not receive
`noalias` or `nocapture`. Apply `readonly` to `BIND(C)` definitions
while leaving external C declarations unannotated. `VALUE`,
`ASYNCHRONOUS`, and `VOLATILE` dummy arguments remain excluded.

In our measurements, the enabled function specialization gives a 1.5x
speedup on 548.exchange2 on NVIDIA Neoverse V2 in a non-LTO
configuration.

Update existing lowering tests to account for the new `fir.read_only`
argument attribute, and add focused coverage:

* `dummy-argument-readonly.f90` checks marker emission for supported
scalar and descriptor arguments, excluded cases, and shallow pointer
semantics.
* `function-attrs-readonly.fir` checks translation from `fir.read_only`
to LLVM `readonly`, including `TARGET`, `BIND(C)`, and descriptor
references.
* `function-attr-readonly.f90` checks the optimization-level pipeline
gate and verifies that `readonly` reaches final LLVM IR.
* `call-copy-in-out.f90` checks that ordinary array copy-out cases
remain unmarked and that pointer-target copy-out is compatible with
shallow descriptor `readonly`.

Added: 
    flang/test/Driver/function-attr-readonly.f90
    flang/test/Lower/dummy-argument-readonly.f90
    flang/test/Transforms/function-attrs-readonly.fir

Modified: 
    flang/include/flang/Optimizer/Dialect/FIROpsSupport.h
    flang/include/flang/Optimizer/Transforms/Passes.td
    flang/lib/Lower/CallInterface.cpp
    flang/lib/Optimizer/Passes/Pipelines.cpp
    flang/lib/Optimizer/Transforms/FunctionAttr.cpp
    flang/test/Lower/HLFIR/structure-constructor.f90
    flang/test/Lower/Intrinsics/dconjg.f90
    flang/test/Lower/Intrinsics/dimag.f90
    flang/test/Lower/Intrinsics/dreal.f90
    flang/test/Lower/OpenMP/optional-argument-map.f90
    flang/test/Lower/OpenMP/wsloop-unstructured.f90
    flang/test/Lower/achar.f90
    flang/test/Lower/arguments.f90
    flang/test/Lower/call-copy-in-out.f90
    flang/test/Lower/dispatch.f90
    flang/test/Lower/polymorphic-temp.f90
    flang/test/Lower/polymorphic.f90
    flang/test/Lower/select-type.f90
    flang/test/Lower/unsigned-ops.f90
    flang/test/Lower/volatile1.f90

Removed: 
    


################################################################################
diff  --git a/flang/include/flang/Optimizer/Dialect/FIROpsSupport.h b/flang/include/flang/Optimizer/Dialect/FIROpsSupport.h
index 9e168ffe90d0d..944b9926387d8 100644
--- a/flang/include/flang/Optimizer/Dialect/FIROpsSupport.h
+++ b/flang/include/flang/Optimizer/Dialect/FIROpsSupport.h
@@ -98,6 +98,16 @@ static constexpr llvm::StringRef getVolatileAttrName() {
   return "fir.volatile";
 }
 
+/// Attribute to mark a dummy data object argument as read-only, i.e. the callee
+/// does not write through this argument. Lowered to the LLVM `readonly`
+/// argument attribute by the FunctionAttr pass. CallInterface currently emits
+/// it for a conservative subset of by-reference INTENT(IN) dummies. The
+/// attribute is shallow: on a reference to a descriptor, it only protects the
+/// descriptor storage and does not imply that the described data is read-only.
+static constexpr llvm::StringRef getReadOnlyAttrName() {
+  return "fir.read_only";
+}
+
 /// Attribute to mark that a function argument is a character dummy procedure.
 /// Character dummy procedure have special ABI constraints.
 static constexpr llvm::StringRef getCharacterProcedureDummyAttrName() {

diff  --git a/flang/include/flang/Optimizer/Transforms/Passes.td b/flang/include/flang/Optimizer/Transforms/Passes.td
index 482260f26751d..ae7977d0670c7 100644
--- a/flang/include/flang/Optimizer/Transforms/Passes.td
+++ b/flang/include/flang/Optimizer/Transforms/Passes.td
@@ -477,6 +477,9 @@ def FunctionAttr : Pass<"function-attr", "mlir::func::FuncOp"> {
        Option<"setNoAlias", "set-noalias", "bool", /*default=*/"false",
               "Set LLVM noalias attribute on function arguments, "
               "if possible">,
+       Option<"setReadOnly", "set-readonly", "bool", /*default=*/"false",
+              "Set LLVM readonly attribute on INTENT(IN) by-reference "
+              "function arguments, if possible">,
   ];
 }
 

diff  --git a/flang/lib/Lower/CallInterface.cpp b/flang/lib/Lower/CallInterface.cpp
index e28a16305cb98..64960b5e09a98 100644
--- a/flang/lib/Lower/CallInterface.cpp
+++ b/flang/lib/Lower/CallInterface.cpp
@@ -221,6 +221,36 @@ asImplicitArg(Fortran::evaluate::characteristics::DummyDataObject &&dummy) {
                                                        std::move(shape)));
 }
 
+/// Return true when this dummy is in the subset for which CallInterface may
+/// safely emit the FIR marker that is later translated to LLVM `readonly`.
+///
+/// For non-descriptor arguments, limit this to non-character intrinsic
+/// scalars. Array and derived-type arguments are excluded because
+/// compiler-generated copy-out may write back through a forwarded dummy
+/// argument without an INTENT contract. INTENT(IN) POINTER and ALLOCATABLE
+/// dummies are also supported: in their case, `readonly` only protects the
+/// descriptor storage, not data addressed by the descriptor. ASYNCHRONOUS
+/// memory may change underneath the callee; VOLATILE+INTENT(IN) is prohibited
+/// by the standard (C870) but is excluded defensively. TARGET does not
+/// invalidate `readonly`, which only constrains writes through the argument.
+static bool dummyArgCanUseLLVMReadonly(
+    const Fortran::evaluate::characteristics::DummyDataObject &obj) {
+  using Attrs = Fortran::evaluate::characteristics::DummyDataObject::Attr;
+  using TypeAttrs = Fortran::evaluate::characteristics::TypeAndShape::Attr;
+  const Fortran::common::TypeCategory category = obj.type.type().category();
+  const bool isSupportedIntrinsicScalar =
+      obj.type.Rank() == 0 && !obj.type.attrs().test(TypeAttrs::AssumedRank) &&
+      category != Fortran::common::TypeCategory::Character &&
+      category != Fortran::common::TypeCategory::Derived;
+  const bool isDescriptorDummy =
+      obj.attrs.test(Attrs::Pointer) || obj.attrs.test(Attrs::Allocatable);
+  return obj.intent == Fortran::common::Intent::In &&
+         (isSupportedIntrinsicScalar || isDescriptorDummy) &&
+         !obj.attrs.test(Attrs::Value) &&
+         !obj.attrs.test(Attrs::Asynchronous) &&
+         !obj.attrs.test(Attrs::Volatile);
+}
+
 static Fortran::evaluate::characteristics::DummyArgument
 asImplicitArg(Fortran::evaluate::characteristics::DummyArgument &&dummy) {
   return Fortran::common::visit(
@@ -1122,8 +1152,15 @@ class Fortran::lower::CallInterfaceImpl {
     } else {
       // non-PDT derived type allowed in implicit interface.
       mlir::Type refType = getRefType(dynamicType, obj);
+      llvm::SmallVector<mlir::NamedAttribute> attrs = dummyNameAttr(entity);
+      // Propagate the INTENT(IN) contract for by-reference dummies handled by
+      // the implicit-interface path.
+      if (dummyArgCanUseLLVMReadonly(obj))
+        attrs.emplace_back(
+            mlir::StringAttr::get(&mlirContext, fir::getReadOnlyAttrName()),
+            mlir::UnitAttr::get(&mlirContext));
       addFirOperand(refType, nextPassedArgPosition(), Property::BaseAddress,
-                    dummyNameAttr(entity));
+                    attrs);
       addPassedArg(PassEntityBy::BaseAddress, entity, characteristics);
     }
   }
@@ -1182,6 +1219,11 @@ class Fortran::lower::CallInterfaceImpl {
           mlir::StringAttr::get(&mlirContext, cuf::getDataAttrName()),
           cuf::getDataAttribute(&mlirContext, obj.cudaDataAttr));
 
+    // Mark the supported subset of INTENT(IN) by-reference dummies readonly
+    // (see dummyArgCanUseLLVMReadonly).
+    if (dummyArgCanUseLLVMReadonly(obj))
+      addMLIRAttr(fir::getReadOnlyAttrName());
+
     // TODO: intents that require special care (e.g finalization)
 
     if (obj.type.corank() > 0)

diff  --git a/flang/lib/Optimizer/Passes/Pipelines.cpp b/flang/lib/Optimizer/Passes/Pipelines.cpp
index 70ef9e9bd17bb..16c2cda5d6d93 100644
--- a/flang/lib/Optimizer/Passes/Pipelines.cpp
+++ b/flang/lib/Optimizer/Passes/Pipelines.cpp
@@ -447,13 +447,14 @@ void createDefaultFIRCodeGenPassPipeline(mlir::PassManager &pm,
   // function specialization is fixed.
   bool setNoAlias = forceNoAlias;
   bool setNoCapture = config.OptLevel.isOptimizingForSpeed();
+  bool setReadOnly = config.OptLevel.isOptimizingForSpeed();
 
   pm.addPass(fir::createFunctionAttr(
       {framePointerKind, config.InstrumentFunctionEntry,
        config.InstrumentFunctionExit, config.NoInfsFPMath, config.NoNaNsFPMath,
        config.ApproxFuncFPMath, config.NoSignedZerosFPMath, config.UnsafeFPMath,
        config.Reciprocals, config.PreferVectorWidth, config.UseSampleProfile,
-       /*tuneCPU=*/"", setNoCapture, setNoAlias}));
+       /*tuneCPU=*/"", setNoCapture, setNoAlias, setReadOnly}));
 
   if (config.EnableOpenMP) {
     pm.addNestedPass<mlir::func::FuncOp>(

diff  --git a/flang/lib/Optimizer/Transforms/FunctionAttr.cpp b/flang/lib/Optimizer/Transforms/FunctionAttr.cpp
index b49803e989265..45b32d13ad62e 100644
--- a/flang/lib/Optimizer/Transforms/FunctionAttr.cpp
+++ b/flang/lib/Optimizer/Transforms/FunctionAttr.cpp
@@ -54,31 +54,46 @@ void FunctionAttrPass::runOnOperation() {
   auto deconstructed = fir::NameUniquer::deconstruct(name);
   bool isFromModule = !deconstructed.second.modules.empty();
 
-  if ((isFromModule || !func.isDeclaration()) &&
-      !fir::hasBindcAttr(func.getOperation())) {
+  if (isFromModule || !func.isDeclaration()) {
+    const bool isBindC = fir::hasBindcAttr(func.getOperation());
+    const bool isDefinition = !func.isDeclaration();
     llvm::StringRef nocapture = mlir::LLVM::LLVMDialect::getNoCaptureAttrName();
     llvm::StringRef noalias = mlir::LLVM::LLVMDialect::getNoAliasAttrName();
+    llvm::StringRef readonly = mlir::LLVM::LLVMDialect::getReadonlyAttrName();
     mlir::UnitAttr unitAttr = mlir::UnitAttr::get(func.getContext());
 
     for (auto [index, argType] : llvm::enumerate(func.getArgumentTypes())) {
       bool isNoCapture = false;
       bool isNoAlias = false;
-      if (mlir::isa<fir::ReferenceType>(argType) &&
-          !func.getArgAttr(index, fir::getTargetAttrName()) &&
-          !func.getArgAttr(index, fir::getAsynchronousAttrName()) &&
-          !func.getArgAttr(index, fir::getVolatileAttrName())) {
-        isNoCapture = true;
-        isNoAlias = !fir::isPointerType(argType);
+      bool isReadOnly = false;
+      if (mlir::isa<fir::ReferenceType>(argType)) {
+        // The read-only marker is attached to by-reference dummies the callee
+        // is guaranteed not to write through (see CallInterface). Unlike
+        // noalias/nocapture, it is valid even for TARGET arguments, so it is
+        // computed independently of the target/asynchronous/volatile gating.
+        isReadOnly = static_cast<bool>(
+            func.getArgAttr(index, fir::getReadOnlyAttrName()));
+        if (!func.getArgAttr(index, fir::getTargetAttrName()) &&
+            !func.getArgAttr(index, fir::getAsynchronousAttrName()) &&
+            !func.getArgAttr(index, fir::getVolatileAttrName())) {
+          isNoCapture = true;
+          isNoAlias = !fir::isPointerType(argType);
+        }
       } else if (mlir::isa<fir::BaseBoxType>(argType)) {
         // !fir.box arguments will be passed as descriptor pointers
         // at LLVM IR dialect level - they cannot be captured,
         // and cannot alias with anything within the function.
         isNoCapture = isNoAlias = true;
       }
-      if (isNoCapture && setNoCapture)
+      // nocapture/noalias are not applied to BIND(C) interfaces (C ABI).
+      if (isNoCapture && setNoCapture && !isBindC)
         func.setArgAttr(index, nocapture, unitAttr);
-      if (isNoAlias && setNoAlias)
+      if (isNoAlias && setNoAlias && !isBindC)
         func.setArgAttr(index, noalias, unitAttr);
+      // readonly is also valid for BIND(C) definitions (the Fortran body must
+      // honor INTENT(IN)), but not for declarations of external C functions.
+      if (isReadOnly && setReadOnly && (!isBindC || isDefinition))
+        func.setArgAttr(index, readonly, unitAttr);
     }
   }
 

diff  --git a/flang/test/Driver/function-attr-readonly.f90 b/flang/test/Driver/function-attr-readonly.f90
new file mode 100644
index 0000000000000..0f7b1b0589a3c
--- /dev/null
+++ b/flang/test/Driver/function-attr-readonly.f90
@@ -0,0 +1,48 @@
+! Verify that the default pipeline translates the lowering marker only at
+! optimization levels that optimize for speed, and that llvm.readonly reaches
+! the final LLVM IR.
+! force-no-alias currently defaults to true, so llvm.noalias is expected at
+! both levels; llvm.nocapture and llvm.readonly are the attributes gated here.
+!
+! RUN: %flang_fc1 -emit-llvm -O0 \
+! RUN:   -mmlir --mlir-disable-threading \
+! RUN:   -mmlir --mlir-print-ir-after=function-attr \
+! RUN:   -mmlir --mlir-print-ir-module-scope %s -o /dev/null 2>&1 \
+! RUN:   | FileCheck %s --check-prefix=O0
+! RUN: %flang_fc1 -emit-llvm -O1 \
+! RUN:   -mmlir --mlir-disable-threading \
+! RUN:   -mmlir --mlir-print-ir-after=function-attr \
+! RUN:   -mmlir --mlir-print-ir-module-scope %s -o /dev/null 2>&1 \
+! RUN:   | FileCheck %s --check-prefix=O1
+! RUN: %flang_fc1 -emit-llvm -O1 %s -o - | FileCheck %s --check-prefix=LLVM
+
+module readonly_pipeline_mod
+contains
+  subroutine readonly_pipeline(x, y)
+    integer, intent(in) :: x
+    integer, intent(inout) :: y
+    y = x
+  end subroutine
+
+  subroutine pointer_descriptor_readonly(p)
+    integer, intent(in), pointer :: p
+    p = 42
+  end subroutine
+end module
+
+! O0-LABEL: func.func @_QMreadonly_pipeline_modPreadonly_pipeline(
+! O0-SAME:    %{{.*}}: !fir.ref<i32> {fir.bindc_name = "x", fir.read_only, llvm.noalias},
+! O0-SAME:    %{{.*}}: !fir.ref<i32> {fir.bindc_name = "y", llvm.noalias}) {
+
+! O1-LABEL: func.func @_QMreadonly_pipeline_modPreadonly_pipeline(
+! O1-SAME:    %{{.*}}: !fir.ref<i32> {fir.bindc_name = "x", fir.read_only, llvm.noalias, llvm.nocapture, llvm.readonly},
+! O1-SAME:    %{{.*}}: !fir.ref<i32> {fir.bindc_name = "y", llvm.noalias, llvm.nocapture}) {
+
+! LLVM-LABEL: define void @_QMreadonly_pipeline_modPreadonly_pipeline(
+! LLVM-SAME:    ptr {{.*}}readonly{{.*}} %0,
+
+! O1-LABEL: func.func @_QMreadonly_pipeline_modPpointer_descriptor_readonly(%{{.*}}: !fir.ref<!fir.box<!fir.ptr<i32>>> {fir.bindc_name = "p", fir.read_only, llvm.nocapture, llvm.readonly}) {
+
+! LLVM-LABEL: define void @_QMreadonly_pipeline_modPpointer_descriptor_readonly(
+! LLVM-SAME:    ptr {{.*}}readonly{{.*}} %0
+! LLVM:         store i32 42

diff  --git a/flang/test/Lower/HLFIR/structure-constructor.f90 b/flang/test/Lower/HLFIR/structure-constructor.f90
index f2d2c094198b3..f937dc0b23d0e 100644
--- a/flang/test/Lower/HLFIR/structure-constructor.f90
+++ b/flang/test/Lower/HLFIR/structure-constructor.f90
@@ -314,7 +314,7 @@ subroutine test7(n)
   x = t7(n)
 end subroutine test7
 ! CHECK-LABEL:   func.func @_QPtest7(
-! CHECK-SAME:                        %[[VAL_0:.*]]: !fir.ref<i32> {fir.bindc_name = "n"}) {
+! CHECK-SAME:                        %[[VAL_0:.*]]: !fir.ref<i32> {fir.bindc_name = "n", fir.read_only}) {
 ! CHECK:           %[[VAL_1:.*]] = fir.alloca !fir.type<_QMtypesTt7{c1:i32,c2:!fir.box<!fir.heap<!fir.array<?xf32>>>}>
 ! CHECK:           %[[VAL_2:.*]]:2 = hlfir.declare %[[VAL_0]] dummy_scope %{{[0-9]+}} arg {{[0-9]+}} {fortran_attrs = #fir.var_attrs<intent_in>, uniq_name = "_QFtest7En"} : (!fir.ref<i32>, !fir.dscope) -> (!fir.ref<i32>, !fir.ref<i32>)
 ! CHECK:           %[[VAL_3:.*]] = fir.alloca !fir.type<_QMtypesTt7{c1:i32,c2:!fir.box<!fir.heap<!fir.array<?xf32>>>}> {bindc_name = "x", uniq_name = "_QFtest7Ex"}

diff  --git a/flang/test/Lower/Intrinsics/dconjg.f90 b/flang/test/Lower/Intrinsics/dconjg.f90
index 0a219565f5bb5..b223259a39feb 100644
--- a/flang/test/Lower/Intrinsics/dconjg.f90
+++ b/flang/test/Lower/Intrinsics/dconjg.f90
@@ -6,7 +6,7 @@ subroutine test_dconjg(r, c)
 
 ! CHECK-LABEL: func @_QPtest_dconjg(
 ! CHECK-SAME: %[[ARG_0:.*]]: !fir.ref<complex<f64>> {fir.bindc_name = "r"},
-! CHECK-SAME: %[[ARG_1:.*]]: !fir.ref<complex<f64>> {fir.bindc_name = "c"}) {
+! CHECK-SAME: %[[ARG_1:.*]]: !fir.ref<complex<f64>> {fir.bindc_name = "c", fir.read_only}) {
 ! CHECK:   %[[DS:.*]] = fir.dummy_scope : !fir.dscope
 ! CHECK:   %[[C_DECL:.*]]:2 = hlfir.declare %[[ARG_1]] dummy_scope %[[DS]] {{.*}}
 ! CHECK:   %[[R_DECL:.*]]:2 = hlfir.declare %[[ARG_0]] dummy_scope %[[DS]] {{.*}}

diff  --git a/flang/test/Lower/Intrinsics/dimag.f90 b/flang/test/Lower/Intrinsics/dimag.f90
index a90fb31e5864b..7826d441b2e9c 100644
--- a/flang/test/Lower/Intrinsics/dimag.f90
+++ b/flang/test/Lower/Intrinsics/dimag.f90
@@ -6,7 +6,7 @@ subroutine test_dimag(r, c)
 
 ! CHECK-LABEL: func @_QPtest_dimag(
 ! CHECK-SAME: %[[ARG_0:.*]]: !fir.ref<f64> {fir.bindc_name = "r"},
-! CHECK-SAME: %[[ARG_1:.*]]: !fir.ref<complex<f64>> {fir.bindc_name = "c"}) {
+! CHECK-SAME: %[[ARG_1:.*]]: !fir.ref<complex<f64>> {fir.bindc_name = "c", fir.read_only}) {
 ! CHECK:   %[[DS:.*]] = fir.dummy_scope : !fir.dscope
 ! CHECK:   %[[C_DECL:.*]]:2 = hlfir.declare %[[ARG_1]] dummy_scope %[[DS]] {{.*}}
 ! CHECK:   %[[R_DECL:.*]]:2 = hlfir.declare %[[ARG_0]] dummy_scope %[[DS]] {{.*}}

diff  --git a/flang/test/Lower/Intrinsics/dreal.f90 b/flang/test/Lower/Intrinsics/dreal.f90
index 13d8cabba9e64..3f72aa7d3d630 100644
--- a/flang/test/Lower/Intrinsics/dreal.f90
+++ b/flang/test/Lower/Intrinsics/dreal.f90
@@ -6,7 +6,7 @@ subroutine test_dreal(r, c)
 
 ! CHECK-LABEL: func.func @_QPtest_dreal(
 ! CHECK-SAME: %[[ARG_0:.*]]: !fir.ref<f64> {fir.bindc_name = "r"},
-! CHECK-SAME: %[[ARG_1:.*]]: !fir.ref<complex<f64>> {fir.bindc_name = "c"}) {
+! CHECK-SAME: %[[ARG_1:.*]]: !fir.ref<complex<f64>> {fir.bindc_name = "c", fir.read_only}) {
 ! CHECK:   %[[DS:.*]] = fir.dummy_scope : !fir.dscope
 ! CHECK:   %[[C:.*]]:2 = hlfir.declare %[[ARG_1]] dummy_scope %[[DS]]
 ! CHECK:   %[[R:.*]]:2 = hlfir.declare %[[ARG_0]] dummy_scope %[[DS]]

diff  --git a/flang/test/Lower/OpenMP/optional-argument-map.f90 b/flang/test/Lower/OpenMP/optional-argument-map.f90
index 863742ce67927..84c2cc76495ae 100644
--- a/flang/test/Lower/OpenMP/optional-argument-map.f90
+++ b/flang/test/Lower/OpenMP/optional-argument-map.f90
@@ -15,7 +15,7 @@ end subroutine test
 end module foo
 
 ! CHECK-LABEL:   func.func @_QMfooPtest(
-! CHECK-SAME:                           %[[VAL_0:.*]]: !fir.ref<i32> {fir.bindc_name = "i"},
+! CHECK-SAME:                           %[[VAL_0:.*]]: !fir.ref<i32> {fir.bindc_name = "i", fir.read_only},
 ! CHECK-SAME:                           %[[VAL_1:.*]]: !fir.box<!fir.array<?xf32>> {fir.bindc_name = "a", fir.optional}) {
 ! CHECK:           %[[VAL_2:.*]] = fir.alloca !fir.box<!fir.array<?xf32>>
 ! CHECK:           %[[VAL_3:.*]]:2 = hlfir.declare %[[VAL_1]] dummy_scope %{{.*}} {fortran_attrs = #fir.var_attrs<intent_inout, optional>, uniq_name = "_QMfooFtestEa"} : (!fir.box<!fir.array<?xf32>>, !fir.dscope) -> (!fir.box<!fir.array<?xf32>>, !fir.box<!fir.array<?xf32>>)

diff  --git a/flang/test/Lower/OpenMP/wsloop-unstructured.f90 b/flang/test/Lower/OpenMP/wsloop-unstructured.f90
index 6174718c08758..fb79bd1c17d77 100644
--- a/flang/test/Lower/OpenMP/wsloop-unstructured.f90
+++ b/flang/test/Lower/OpenMP/wsloop-unstructured.f90
@@ -24,8 +24,8 @@ end subroutine sub
 ! that all blocks are terminated
 
 ! CHECK-LABEL:   func.func @_QPsub(
-! CHECK-SAME:                      %[[VAL_0:.*]]: !fir.ref<i32> {fir.bindc_name = "imax"},
-! CHECK-SAME:                      %[[VAL_1:.*]]: !fir.ref<i32> {fir.bindc_name = "jmax"},
+! CHECK-SAME:                      %[[VAL_0:.*]]: !fir.ref<i32> {fir.bindc_name = "imax", fir.read_only},
+! CHECK-SAME:                      %[[VAL_1:.*]]: !fir.ref<i32> {fir.bindc_name = "jmax", fir.read_only},
 ! CHECK-SAME:                      %[[VAL_2:.*]]: !fir.ref<!fir.array<?x?xf32>> {fir.bindc_name = "x"},
 ! CHECK-SAME:                      %[[VAL_3:.*]]: !fir.ref<!fir.array<?x?xf32>> {fir.bindc_name = "y"}) {
 ! [...]

diff  --git a/flang/test/Lower/achar.f90 b/flang/test/Lower/achar.f90
index 63bea0076c597..ae35db348a33d 100644
--- a/flang/test/Lower/achar.f90
+++ b/flang/test/Lower/achar.f90
@@ -12,7 +12,7 @@ subroutine achar_test1(a)
 end subroutine achar_test1
 
 ! CHECK-LABEL: func.func @_QPachar_test1(
-! CHECK-SAME: %[[ARG:.*]]: !fir.ref<i32> {fir.bindc_name = "a"}) {
+! CHECK-SAME: %[[ARG:.*]]: !fir.ref<i32> {fir.bindc_name = "a", fir.read_only}) {
 ! CHECK: %[[TMP:.*]] = fir.alloca !fir.char<1>
 ! CHECK: %[[DSCOPE:.*]] = fir.dummy_scope : !fir.dscope
 ! CHECK: %[[A:.*]]:2 = hlfir.declare %[[ARG]] dummy_scope %[[DSCOPE]] arg 1 {fortran_attrs = #fir.var_attrs<intent_in>, uniq_name = "_QFachar_test1Ea"}

diff  --git a/flang/test/Lower/arguments.f90 b/flang/test/Lower/arguments.f90
index f54fa6868d554..9667494f0f011 100644
--- a/flang/test/Lower/arguments.f90
+++ b/flang/test/Lower/arguments.f90
@@ -7,7 +7,7 @@ subroutine sub1(a, b)
 
 ! Check that arguments are correctly set and no local allocation is happening.
 ! CHECK-LABEL: func @_QPsub1(
-! CHECK-SAME:    %{{.*}}: !fir.ref<i32> {fir.bindc_name = "a"}, %{{.*}}: !fir.ref<!fir.logical<4>> {fir.bindc_name = "b"})
+! CHECK-SAME:    %{{.*}}: !fir.ref<i32> {fir.bindc_name = "a", fir.read_only}, %{{.*}}: !fir.ref<!fir.logical<4>> {fir.bindc_name = "b"})
 ! CHECK-NOT:     fir.alloc
 ! CHECK:         return
 
@@ -31,7 +31,7 @@ integer function fct1(a, b)
 end
 
 ! CHECK-LABEL: func @_QPfct1(
-! CHECK-SAME:    %{{.*}}: !fir.ref<i32> {fir.bindc_name = "a"}, %{{.*}}: !fir.ref<!fir.logical<4>> {fir.bindc_name = "b"}) -> i32
+! CHECK-SAME:    %{{.*}}: !fir.ref<i32> {fir.bindc_name = "a", fir.read_only}, %{{.*}}: !fir.ref<!fir.logical<4>> {fir.bindc_name = "b"}) -> i32
 
 real function fct2(i)
   integer :: i(2, 5)

diff  --git a/flang/test/Lower/call-copy-in-out.f90 b/flang/test/Lower/call-copy-in-out.f90
index 15feb390171bf..c869923e75e96 100644
--- a/flang/test/Lower/call-copy-in-out.f90
+++ b/flang/test/Lower/call-copy-in-out.f90
@@ -98,6 +98,29 @@ subroutine test_actual_arg_intent_in(x)
   call bar(x)
 end subroutine
 
+! Test the transitive copy-out case. The outer INTENT(IN) array is forwarded
+! to a dummy without INTENT, which then passes a noncontiguous section to an
+! implicit-interface procedure. Lowering cannot recognize the outer contract
+! at the inner call site, so the inner procedure still generates copy-back.
+! The outer array must therefore not be marked fir.read_only.
+! CHECK-LABEL: func.func @_QPtest_forwarded_intent_in(
+! CHECK-SAME: %{{.*}}: !fir.ref<!fir.array<4xf32>> {fir.bindc_name = "x"}) {
+subroutine test_forwarded_intent_in(x)
+  real, intent(in) :: x(4)
+  call test_forwarded_without_intent(x)
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_forwarded_without_intent(
+! CHECK-SAME: %{{.*}}: !fir.ref<!fir.array<4xf32>> {fir.bindc_name = "x"}) {
+subroutine test_forwarded_without_intent(x)
+  real :: x(4)
+! CHECK: hlfir.copy_in
+! CHECK: fir.call @_QPbar
+! CHECK: hlfir.copy_out
+! CHECK-SAME: to
+  call bar(x(1:4:2))
+end subroutine
+
 ! Test copy-out is NOT skipped when passing a section of a pointer component
 ! of an INTENT(IN) dummy: the pointer target is not a subobject of the dummy
 ! (F2023 9.4.2 p5), so the callee may define it and copy-out is required.
@@ -118,7 +141,8 @@ subroutine test_actual_arg_intent_in_ptr_component(x)
 ! pointer dummy: INTENT(IN) on a pointer restricts pointer association, not
 ! the target's contents, so the callee may define the target and copy-out is
 ! required.
-! CHECK-LABEL: func @_QPtest_actual_intent_in_pointer(
+! CHECK-LABEL: func.func @_QPtest_actual_intent_in_pointer(
+! CHECK-SAME: %{{.*}}: !fir.ref<!fir.box<!fir.ptr<!fir.array<?xi32>>>> {fir.bindc_name = "pi", fir.read_only}) {
 subroutine test_actual_intent_in_pointer(pi)
   integer, intent(in), pointer :: pi(:)
 ! CHECK: hlfir.copy_in

diff  --git a/flang/test/Lower/dispatch.f90 b/flang/test/Lower/dispatch.f90
index 7692bd31d4533..a66287d2b4fd6 100644
--- a/flang/test/Lower/dispatch.f90
+++ b/flang/test/Lower/dispatch.f90
@@ -107,7 +107,7 @@ subroutine p1_proc4_arg1(i, this)
       integer, intent(in) :: i
       class(p1) :: this
     end subroutine
-    ! CHECK-LABEL: func.func @_QMcall_dispatchPp1_proc4_arg1(%{{.*}}: !fir.ref<i32> {fir.bindc_name = "i"}, %{{.*}}: !fir.class<!fir.type<_QMcall_dispatchTp1{a:i32,b:i32}>> {fir.bindc_name = "this"})
+    ! CHECK-LABEL: func.func @_QMcall_dispatchPp1_proc4_arg1(%{{.*}}: !fir.ref<i32> {fir.bindc_name = "i", fir.read_only}, %{{.*}}: !fir.class<!fir.type<_QMcall_dispatchTp1{a:i32,b:i32}>> {fir.bindc_name = "this"})
 
     subroutine tbp_nopass()
     end subroutine
@@ -127,7 +127,7 @@ subroutine tbp_pass_arg1(i, this)
       integer, intent(in) :: i
       class(p1) :: this
     end subroutine
-    ! CHECK-LABEL: func.func @_QMcall_dispatchPtbp_pass_arg1(%{{.*}}: !fir.ref<i32> {fir.bindc_name = "i"}, %{{.*}}: !fir.class<!fir.type<_QMcall_dispatchTp1{a:i32,b:i32}>> {fir.bindc_name = "this"})
+    ! CHECK-LABEL: func.func @_QMcall_dispatchPtbp_pass_arg1(%{{.*}}: !fir.ref<i32> {fir.bindc_name = "i", fir.read_only}, %{{.*}}: !fir.class<!fir.type<_QMcall_dispatchTp1{a:i32,b:i32}>> {fir.bindc_name = "this"})
 
     subroutine check_dispatch(p)
       class(p1) :: p

diff  --git a/flang/test/Lower/dummy-argument-readonly.f90 b/flang/test/Lower/dummy-argument-readonly.f90
new file mode 100644
index 0000000000000..97ae5bea85227
--- /dev/null
+++ b/flang/test/Lower/dummy-argument-readonly.f90
@@ -0,0 +1,175 @@
+! Test the fir.read_only marker produced by CallInterface for the supported
+! subset of INTENT(IN) dummy data objects. The marker is emitted during
+! lowering independently of the optimization level.
+!
+! RUN: %flang_fc1 -emit-hlfir %s -o - | FileCheck %s
+
+subroutine scalar_intent_in(x)
+  integer, intent(in) :: x
+end subroutine
+! CHECK-LABEL: func.func @_QPscalar_intent_in(
+! CHECK-SAME:    %{{.*}}: !fir.ref<i32> {fir.bindc_name = "x", fir.read_only}) {
+
+subroutine real_scalar_intent_in(x)
+  real, intent(in) :: x
+end subroutine
+! CHECK-LABEL: func.func @_QPreal_scalar_intent_in(
+! CHECK-SAME:    %{{.*}}: !fir.ref<f32> {fir.bindc_name = "x", fir.read_only}) {
+
+subroutine complex_scalar_intent_in(x)
+  complex, intent(in) :: x
+end subroutine
+! CHECK-LABEL: func.func @_QPcomplex_scalar_intent_in(
+! CHECK-SAME:    %{{.*}}: !fir.ref<complex<f32>> {fir.bindc_name = "x", fir.read_only}) {
+
+subroutine logical_scalar_intent_in(x)
+  logical, intent(in) :: x
+end subroutine
+! CHECK-LABEL: func.func @_QPlogical_scalar_intent_in(
+! CHECK-SAME:    %{{.*}}: !fir.ref<!fir.logical<4>> {fir.bindc_name = "x", fir.read_only}) {
+
+recursive subroutine recursive_scalar_intent_in(x)
+  integer, intent(in) :: x
+  if (x > 0) call recursive_scalar_intent_in(x - 1)
+end subroutine
+! CHECK-LABEL: func.func @_QPrecursive_scalar_intent_in(
+! CHECK-SAME:    %{{.*}}: !fir.ref<i32> {fir.bindc_name = "x", fir.read_only}) attributes
+
+subroutine optional_scalar_intent_in(x)
+  integer, intent(in), optional :: x
+end subroutine
+! CHECK-LABEL: func.func @_QPoptional_scalar_intent_in(
+! CHECK-SAME:    %{{.*}}: !fir.ref<i32> {fir.bindc_name = "x", fir.optional, fir.read_only}) {
+
+subroutine target_scalar_intent_in(x)
+  integer, intent(in), target :: x
+end subroutine
+! CHECK-LABEL: func.func @_QPtarget_scalar_intent_in(
+! CHECK-SAME:    %{{.*}}: !fir.ref<i32> {fir.bindc_name = "x", fir.read_only, fir.target}) {
+
+subroutine bindc_definition_intent_in(x) bind(c)
+  use iso_c_binding, only : c_int
+  integer(c_int), intent(in) :: x
+end subroutine
+! CHECK-LABEL: func.func @bindc_definition_intent_in(
+! CHECK-SAME:    %{{.*}}: !fir.ref<i32> {fir.bindc_name = "x", fir.read_only}) attributes
+
+! Arrays are excluded because a forwarded dummy without an INTENT contract may
+! require compiler-generated copy-out.
+subroutine explicit_shape_intent_in(x)
+  integer, intent(in) :: x(10)
+end subroutine
+! CHECK-LABEL: func.func @_QPexplicit_shape_intent_in(
+! CHECK-SAME:    %{{.*}}: !fir.ref<!fir.array<10xi32>> {fir.bindc_name = "x"}) {
+
+subroutine assumed_size_intent_in(x)
+  integer, intent(in) :: x(*)
+end subroutine
+! CHECK-LABEL: func.func @_QPassumed_size_intent_in(
+! CHECK-SAME:    %{{.*}}: !fir.ref<!fir.array<?xi32>> {fir.bindc_name = "x"}) {
+
+subroutine target_explicit_shape_intent_in(x)
+  integer, intent(in), target :: x(10)
+end subroutine
+! CHECK-LABEL: func.func @_QPtarget_explicit_shape_intent_in(
+! CHECK-SAME:    %{{.*}}: !fir.ref<!fir.array<10xi32>> {fir.bindc_name = "x", fir.target}) {
+
+subroutine assumed_shape_intent_in(x)
+  integer, intent(in) :: x(:)
+end subroutine
+! CHECK-LABEL: func.func @_QPassumed_shape_intent_in(
+! CHECK-SAME:    %{{.*}}: !fir.box<!fir.array<?xi32>> {fir.bindc_name = "x"}) {
+
+subroutine assumed_rank_intent_in(x)
+  integer, intent(in) :: x(..)
+end subroutine
+! CHECK-LABEL: func.func @_QPassumed_rank_intent_in(
+! CHECK-SAME:    %{{.*}}: !fir.box<!fir.array<*:i32>> {fir.bindc_name = "x"}) {
+
+! CHARACTER and derived types are outside the current conservative subset.
+subroutine character_intent_in(x)
+  character(len=*), intent(in) :: x
+end subroutine
+! CHECK-LABEL: func.func @_QPcharacter_intent_in(
+! CHECK-SAME:    %{{.*}}: !fir.boxchar<1> {fir.bindc_name = "x"}) {
+
+module readonly_derived_types
+  type :: type_with_array
+    real :: values(4)
+  end type
+contains
+  subroutine derived_with_array_intent_in(x)
+    type(type_with_array), intent(in) :: x
+  end subroutine
+! CHECK-LABEL: func.func @_QMreadonly_derived_typesPderived_with_array_intent_in(
+! CHECK-SAME:    %{{.*}}: !fir.ref<!fir.type<{{.*}}>> {fir.bindc_name = "x"}) {
+end module
+
+subroutine intent_inout(x)
+  integer, intent(inout) :: x
+end subroutine
+! CHECK-LABEL: func.func @_QPintent_inout(
+! CHECK-SAME:    %{{.*}}: !fir.ref<i32> {fir.bindc_name = "x"}) {
+
+subroutine intent_out(x)
+  integer, intent(out) :: x
+end subroutine
+! CHECK-LABEL: func.func @_QPintent_out(
+! CHECK-SAME:    %{{.*}}: !fir.ref<i32> {fir.bindc_name = "x"}) {
+
+subroutine intent_unspecified(x)
+  integer :: x
+end subroutine
+! CHECK-LABEL: func.func @_QPintent_unspecified(
+! CHECK-SAME:    %{{.*}}: !fir.ref<i32> {fir.bindc_name = "x"}) {
+
+subroutine intent_in_value(x)
+  integer, intent(in), value :: x
+end subroutine
+! CHECK-LABEL: func.func @_QPintent_in_value(
+! CHECK-SAME:    %{{.*}}: i32 {fir.bindc_name = "x"}) {
+
+subroutine intent_in_pointer(x)
+  integer, intent(in), pointer :: x
+end subroutine
+! CHECK-LABEL: func.func @_QPintent_in_pointer(
+! CHECK-SAME:    %{{.*}}: !fir.ref<!fir.box<!fir.ptr<i32>>> {fir.bindc_name = "x", fir.read_only}) {
+
+subroutine intent_in_allocatable(x)
+  integer, intent(in), allocatable :: x
+end subroutine
+! CHECK-LABEL: func.func @_QPintent_in_allocatable(
+! CHECK-SAME:    %{{.*}}: !fir.ref<!fir.box<!fir.heap<i32>>> {fir.bindc_name = "x", fir.read_only}) {
+
+subroutine intent_in_pointer_array(x)
+  integer, intent(in), pointer :: x(:)
+end subroutine
+! CHECK-LABEL: func.func @_QPintent_in_pointer_array(
+! CHECK-SAME:    %{{.*}}: !fir.ref<!fir.box<!fir.ptr<!fir.array<?xi32>>>> {fir.bindc_name = "x", fir.read_only}) {
+
+subroutine intent_in_allocatable_array(x)
+  integer, intent(in), allocatable :: x(:)
+end subroutine
+! CHECK-LABEL: func.func @_QPintent_in_allocatable_array(
+! CHECK-SAME:    %{{.*}}: !fir.ref<!fir.box<!fir.heap<!fir.array<?xi32>>>> {fir.bindc_name = "x", fir.read_only}) {
+
+! The marker is shallow for descriptor arguments. Defining a POINTER target is
+! valid even though the descriptor itself has INTENT(IN).
+subroutine intent_in_pointer_target_write(x)
+  integer, intent(in), pointer :: x
+  x = 42
+end subroutine
+! CHECK-LABEL: func.func @_QPintent_in_pointer_target_write(
+! CHECK-SAME:    %{{.*}}: !fir.ref<!fir.box<!fir.ptr<i32>>> {fir.bindc_name = "x", fir.read_only}) {
+! CHECK:         hlfir.assign {{.*}} to {{.*}} : i32, !fir.ptr<i32>
+
+subroutine intent_in_asynchronous(x)
+  integer, intent(in), asynchronous :: x
+end subroutine
+! CHECK-LABEL: func.func @_QPintent_in_asynchronous(
+! CHECK-SAME:    %{{.*}}: !fir.ref<i32> {fir.asynchronous, fir.bindc_name = "x"}) {
+
+! VOLATILE with INTENT(IN) is prohibited by C870 (Fortran 2023) and is already
+! covered by test/Semantics/misc-declarations.f90, so it cannot be exercised by
+! a valid lowering test. The Volatile exclusion in
+! dummyArgCanUseLLVMReadonly remains a defensive check.

diff  --git a/flang/test/Lower/polymorphic-temp.f90 b/flang/test/Lower/polymorphic-temp.f90
index 394c8e2069f2e..cb52a1b9f5581 100644
--- a/flang/test/Lower/polymorphic-temp.f90
+++ b/flang/test/Lower/polymorphic-temp.f90
@@ -208,7 +208,7 @@ subroutine test_merge_intrinsic2(a, b, i)
   end subroutine
 
 ! CHECK-LABEL: func.func @_QMpoly_tmpPtest_merge_intrinsic2(
-! CHECK-SAME: %[[A:.*]]: !fir.ref<!fir.class<!fir.heap<!fir.type<_QMpoly_tmpTp1{a:i32}>>>> {fir.bindc_name = "a"}, %[[B:.*]]: !fir.ref<!fir.box<!fir.heap<!fir.type<_QMpoly_tmpTp1{a:i32}>>>> {fir.bindc_name = "b"}, %[[I:.*]]: !fir.ref<i32> {fir.bindc_name = "i"}) {
+! CHECK-SAME: %[[A:.*]]: !fir.ref<!fir.class<!fir.heap<!fir.type<_QMpoly_tmpTp1{a:i32}>>>> {fir.bindc_name = "a", fir.read_only}, %[[B:.*]]: !fir.ref<!fir.box<!fir.heap<!fir.type<_QMpoly_tmpTp1{a:i32}>>>> {fir.bindc_name = "b"}, %[[I:.*]]: !fir.ref<i32> {fir.bindc_name = "i", fir.read_only}) {
 ! CHECK: %[[A_DECL:.*]]:2 = hlfir.declare %[[A]]
 ! CHECK: %[[B_DECL:.*]]:2 = hlfir.declare %[[B]]
 ! CHECK: %[[I_DECL:.*]]:2 = hlfir.declare %[[I]]
@@ -230,7 +230,7 @@ subroutine test_merge_intrinsic3(a, b, i)
   end subroutine
 
 ! CHECK-LABEL: func.func @_QMpoly_tmpPtest_merge_intrinsic3(
-! CHECK-SAME: %[[A:.*]]: !fir.class<none> {fir.bindc_name = "a"}, %[[B:.*]]: !fir.class<none> {fir.bindc_name = "b"}, %[[I:.*]]: !fir.ref<i32> {fir.bindc_name = "i"}) {
+! CHECK-SAME: %[[A:.*]]: !fir.class<none> {fir.bindc_name = "a"}, %[[B:.*]]: !fir.class<none> {fir.bindc_name = "b"}, %[[I:.*]]: !fir.ref<i32> {fir.bindc_name = "i", fir.read_only}) {
 ! CHECK: %[[A_DECL:.*]]:2 = hlfir.declare %[[A]]
 ! CHECK: %[[B_DECL:.*]]:2 = hlfir.declare %[[B]]
 ! CHECK: %[[I_DECL:.*]]:2 = hlfir.declare %[[I]]
@@ -247,7 +247,7 @@ subroutine test_merge_intrinsic4(i)
   end subroutine
 
 ! CHECK-LABEL: func.func @_QMpoly_tmpPtest_merge_intrinsic4(
-! CHECK-SAME: %[[I:.*]]: !fir.ref<i32> {fir.bindc_name = "i"}) {
+! CHECK-SAME: %[[I:.*]]: !fir.ref<i32> {fir.bindc_name = "i", fir.read_only}) {
 ! CHECK: %[[V_0:[0-9]+]] = fir.alloca !fir.class<!fir.heap<none>> {bindc_name = "a", uniq_name = "_QMpoly_tmpFtest_merge_intrinsic4Ea"}
 ! CHECK: %[[V_1:[0-9]+]] = fir.zero_bits !fir.heap<none>
 ! CHECK: %[[V_2:[0-9]+]] = fir.embox %[[V_1]] : (!fir.heap<none>) -> !fir.class<!fir.heap<none>>
@@ -274,7 +274,7 @@ subroutine test_merge_intrinsic5(i)
   end subroutine
 
 ! CHECK-LABEL: func.func @_QMpoly_tmpPtest_merge_intrinsic5(
-! CHECK-SAME: %[[I:.*]]: !fir.ref<i32> {fir.bindc_name = "i"}) {
+! CHECK-SAME: %[[I:.*]]: !fir.ref<i32> {fir.bindc_name = "i", fir.read_only}) {
 ! CHECK: %[[V_0:[0-9]+]] = fir.alloca !fir.class<!fir.ptr<none>> {bindc_name = "a", uniq_name = "_QMpoly_tmpFtest_merge_intrinsic5Ea"}
 ! CHECK: %[[V_1:[0-9]+]] = fir.zero_bits !fir.ptr<none>
 ! CHECK: %[[V_2:[0-9]+]] = fir.embox %[[V_1]] : (!fir.ptr<none>) -> !fir.class<!fir.ptr<none>>

diff  --git a/flang/test/Lower/polymorphic.f90 b/flang/test/Lower/polymorphic.f90
index cd9a9a213e354..3e47463222077 100644
--- a/flang/test/Lower/polymorphic.f90
+++ b/flang/test/Lower/polymorphic.f90
@@ -66,7 +66,7 @@ elemental subroutine assign_p1_int(lhs, rhs)
     lhs%b = rhs
   End Subroutine
 ! CHECK-LABEL: func.func @_QMpolymorphic_testPassign_p1_int(
-! CHECK-SAME:    %[[ARG0:.*]]: !fir.class<!fir.type<_QMpolymorphic_testTp1{a:i32,b:i32}>> {fir.bindc_name = "lhs"}, %[[ARG1:.*]]: !fir.ref<i32> {fir.bindc_name = "rhs"}
+! CHECK-SAME:    %[[ARG0:.*]]: !fir.class<!fir.type<_QMpolymorphic_testTp1{a:i32,b:i32}>> {fir.bindc_name = "lhs"}, %[[ARG1:.*]]: !fir.ref<i32> {fir.bindc_name = "rhs", fir.read_only}
 ! CHECK-SAME:    attributes {fir.proc_attrs = #fir.proc_attrs<elemental, pure>}
 ! CHECK:         %[[LHS:.*]]:2 = hlfir.declare %[[ARG0]]{{.*}}{fortran_attrs = #fir.var_attrs<intent_inout>, uniq_name = "_QMpolymorphic_testFassign_p1_intElhs"}
 ! CHECK:         %[[RHS:.*]]:2 = hlfir.declare %[[ARG1]]{{.*}}{fortran_attrs = #fir.var_attrs<intent_in>, uniq_name = "_QMpolymorphic_testFassign_p1_intErhs"}
@@ -102,7 +102,7 @@ elemental subroutine elemental_sub_pass(c, this)
     this%a = this%a * this%b + c
   end subroutine
 ! CHECK-LABEL: func.func @_QMpolymorphic_testPelemental_sub_pass(
-! CHECK-SAME:    %[[ARG0:.*]]: !fir.ref<i32> {fir.bindc_name = "c"}, %[[ARG1:.*]]: !fir.class<!fir.type<_QMpolymorphic_testTp1{a:i32,b:i32}>> {fir.bindc_name = "this"})
+! CHECK-SAME:    %[[ARG0:.*]]: !fir.ref<i32> {fir.bindc_name = "c", fir.read_only}, %[[ARG1:.*]]: !fir.class<!fir.type<_QMpolymorphic_testTp1{a:i32,b:i32}>> {fir.bindc_name = "this"})
 ! CHECK-SAME:    attributes {fir.proc_attrs = #fir.proc_attrs<elemental, pure>}
 
   logical elemental function lt(i, poly)
@@ -111,7 +111,7 @@ logical elemental function lt(i, poly)
     lt = i < poly%a
   End Function
 ! CHECK-LABEL: func.func @_QMpolymorphic_testPlt(
-! CHECK-SAME:    %[[ARG0:.*]]: !fir.ref<i32> {fir.bindc_name = "i"}, %[[ARG1:.*]]: !fir.class<!fir.type<_QMpolymorphic_testTp1{a:i32,b:i32}>> {fir.bindc_name = "poly"})
+! CHECK-SAME:    %[[ARG0:.*]]: !fir.ref<i32> {fir.bindc_name = "i", fir.read_only}, %[[ARG1:.*]]: !fir.class<!fir.type<_QMpolymorphic_testTp1{a:i32,b:i32}>> {fir.bindc_name = "poly"})
 ! CHECK-SAME:    -> !fir.logical<4> attributes {fir.proc_attrs = #fir.proc_attrs<elemental, pure>}
 ! CHECK:         %[[I:.*]]:2 = hlfir.declare %[[ARG0]]{{.*}}{fortran_attrs = #fir.var_attrs<intent_in>, uniq_name = "_QMpolymorphic_testFltEi"}
 ! CHECK:         %[[POLY:.*]]:2 = hlfir.declare %[[ARG1]]{{.*}}{fortran_attrs = #fir.var_attrs<intent_in>, uniq_name = "_QMpolymorphic_testFltEpoly"}
@@ -680,7 +680,7 @@ subroutine opt_int(i)
     call opt_up(i)
   end subroutine
 ! CHECK-LABEL: func.func @_QMpolymorphic_testPopt_int(
-! CHECK-SAME:    %[[ARG0:.*]]: !fir.ref<i32> {fir.bindc_name = "i", fir.optional}
+! CHECK-SAME:    %[[ARG0:.*]]: !fir.ref<i32> {fir.bindc_name = "i", fir.optional, fir.read_only}
 
   subroutine opt_up(up)
     class(*), optional, intent(in) :: up
@@ -714,7 +714,7 @@ subroutine up_pointer(p)
     class(*), pointer, intent(in) :: p
   end subroutine
 ! CHECK-LABEL: func.func @_QMpolymorphic_testPup_pointer(
-! CHECK-SAME:    %[[ARG0:.*]]: !fir.ref<!fir.class<!fir.ptr<none>>> {fir.bindc_name = "p"}
+! CHECK-SAME:    %[[ARG0:.*]]: !fir.ref<!fir.class<!fir.ptr<none>>> {fir.bindc_name = "p", fir.read_only}
 
   subroutine test_char_to_up_pointer(c)
     character(*), target :: c
@@ -884,4 +884,3 @@ program test
 
   l = i < o%inner
 end program
-

diff  --git a/flang/test/Lower/select-type.f90 b/flang/test/Lower/select-type.f90
index 1c6bb5ce0d4f6..edec4e276bd1b 100644
--- a/flang/test/Lower/select-type.f90
+++ b/flang/test/Lower/select-type.f90
@@ -166,7 +166,7 @@ subroutine select_type3(a)
   end subroutine
 
 ! CHECK-LABEL: func.func @_QMselect_type_lower_testPselect_type3(
-! CHECK-SAME: %[[ARG0:.*]]: !fir.ref<!fir.class<!fir.ptr<!fir.array<?x!fir.type<_QMselect_type_lower_testTp1{a:i32,b:i32}>>>>> {fir.bindc_name = "a"})
+! CHECK-SAME: %[[ARG0:.*]]: !fir.ref<!fir.class<!fir.ptr<!fir.array<?x!fir.type<_QMselect_type_lower_testTp1{a:i32,b:i32}>>>>> {fir.bindc_name = "a", fir.read_only})
 ! CHECK: %[[A:.*]]:2 = hlfir.declare %[[ARG0]]{{.*}}{fortran_attrs = #fir.var_attrs<intent_in, pointer>, uniq_name = "_QMselect_type_lower_testFselect_type3Ea"}
 ! CHECK: %[[ARG0_LOAD:.*]] = fir.load %[[A]]#0 : !fir.ref<!fir.class<!fir.ptr<!fir.array<?x!fir.type<_QMselect_type_lower_testTp1{a:i32,b:i32}>>>>>
 ! CHECK: %[[SELECTOR:.*]] = hlfir.designate %[[ARG0_LOAD]] (%{{.*}})  : (!fir.class<!fir.ptr<!fir.array<?x!fir.type<_QMselect_type_lower_testTp1{a:i32,b:i32}>>>>, index) -> !fir.class<!fir.type<_QMselect_type_lower_testTp1{a:i32,b:i32}>>
@@ -177,7 +177,7 @@ subroutine select_type3(a)
 ! CHECK: ^[[DEFAULT_BLK]]
 
 ! CFG-LABEL: func.func @_QMselect_type_lower_testPselect_type3(
-! CFG-SAME: %[[ARG0:.*]]: !fir.ref<!fir.class<!fir.ptr<!fir.array<?x!fir.type<_QMselect_type_lower_testTp1{a:i32,b:i32}>>>>> {fir.bindc_name = "a"}) {
+! CFG-SAME: %[[ARG0:.*]]: !fir.ref<!fir.class<!fir.ptr<!fir.array<?x!fir.type<_QMselect_type_lower_testTp1{a:i32,b:i32}>>>>> {fir.bindc_name = "a", fir.read_only}) {
 ! CFG:      %[[SELECTOR:.*]] = hlfir.designate %{{.*}} (%{{.*}})  : (!fir.class<!fir.ptr<!fir.array<?x!fir.type<_QMselect_type_lower_testTp1{a:i32,b:i32}>>>>, index) -> !fir.class<!fir.type<_QMselect_type_lower_testTp1{a:i32,b:i32}>>
 ! CFG:      %[[TDESC_P1_ADDR:.*]] = fir.type_desc !fir.type<_QMselect_type_lower_testTp1{a:i32,b:i32}>
 ! CFG:      %[[SELECTOR_TDESC:.*]] = fir.box_tdesc %[[SELECTOR]] : (!fir.class<!fir.type<_QMselect_type_lower_testTp1{a:i32,b:i32}>>) -> !fir.tdesc<{{.*}}>

diff  --git a/flang/test/Lower/unsigned-ops.f90 b/flang/test/Lower/unsigned-ops.f90
index 644f0c4ea11c9..ff6704e37d77b 100644
--- a/flang/test/Lower/unsigned-ops.f90
+++ b/flang/test/Lower/unsigned-ops.f90
@@ -5,7 +5,7 @@ unsigned function f01(u, v)
   f01 = u + v - 1u
 end
 
-!CHECK: func.func @_QPf01(%[[ARG0:.*]]: !fir.ref<ui32> {fir.bindc_name = "u"}, %[[ARG1:.*]]: !fir.ref<ui32> {fir.bindc_name = "v"}) -> ui32 {
+!CHECK: func.func @_QPf01(%[[ARG0:.*]]: !fir.ref<ui32> {fir.bindc_name = "u", fir.read_only}, %[[ARG1:.*]]: !fir.ref<ui32> {fir.bindc_name = "v", fir.read_only}) -> ui32 {
 !CHECK: %[[C1_I32:.*]] = arith.constant 1 : i32
 !CHECK: %[[VAL_0:.*]] = fir.dummy_scope : !fir.dscope
 !CHECK: %[[VAL_1:.*]] = fir.alloca ui32 {bindc_name = "f01", uniq_name = "_QFf01Ef01"}
@@ -30,7 +30,7 @@ unsigned function f02(u, v)
   f02 = u ** v - 1u
 end
 
-!CHECK: func.func @_QPf02(%[[ARG0:.*]]: !fir.ref<ui32> {fir.bindc_name = "u"}, %[[ARG1:.*]]: !fir.ref<ui32> {fir.bindc_name = "v"}) -> ui32 {
+!CHECK: func.func @_QPf02(%[[ARG0:.*]]: !fir.ref<ui32> {fir.bindc_name = "u", fir.read_only}, %[[ARG1:.*]]: !fir.ref<ui32> {fir.bindc_name = "v", fir.read_only}) -> ui32 {
 !CHECK: %[[C1_i32:.*]] = arith.constant 1 : i32
 !CHECK: %[[VAL_0:.*]] = fir.dummy_scope : !fir.dscope
 !CHECK: %[[VAL_1:.*]] = fir.alloca ui32 {bindc_name = "f02", uniq_name = "_QFf02Ef02"}
@@ -49,4 +49,3 @@ unsigned function f02(u, v)
 !CHECK: fir.store %[[VAL_13]] to %[[VAL_2]] : !fir.ref<ui32>
 !CHECK: %[[VAL_14:.*]] = fir.load %[[VAL_2]] : !fir.ref<ui32>
 !CHECK: return %[[VAL_14]] : ui32
-

diff  --git a/flang/test/Lower/volatile1.f90 b/flang/test/Lower/volatile1.f90
index ec1c85eb8e981..8d7c71fd0e05c 100644
--- a/flang/test/Lower/volatile1.f90
+++ b/flang/test/Lower/volatile1.f90
@@ -76,7 +76,7 @@ subroutine declared_volatile_in_this_scope(v,n)
 
 ! CHECK-LABEL:   func.func private @_QFPdeclared_volatile_in_this_scope(
 ! CHECK-SAME:                                                           %[[VAL_0:[0-9]+|[a-zA-Z$._-][a-zA-Z0-9$._-]*]]: !fir.ref<!fir.array<?xi32>> {fir.bindc_name = "v"},
-! CHECK-SAME:                                                           %[[VAL_1:[0-9]+|[a-zA-Z$._-][a-zA-Z0-9$._-]*]]: !fir.ref<i32> {fir.bindc_name = "n"}) attributes {{.+}} {
+! CHECK-SAME:                                                           %[[VAL_1:[0-9]+|[a-zA-Z$._-][a-zA-Z0-9$._-]*]]: !fir.ref<i32> {fir.bindc_name = "n", fir.read_only}) attributes {{.+}} {
 ! CHECK:           %[[VAL_2:.*]] = arith.constant 1 : i32
 ! CHECK:           %[[VAL_3:.*]] = arith.constant 0 : index
 ! CHECK:           %[[VAL_4:.*]] = fir.dummy_scope : !fir.dscope

diff  --git a/flang/test/Transforms/function-attrs-readonly.fir b/flang/test/Transforms/function-attrs-readonly.fir
new file mode 100644
index 0000000000000..7f68424dc1283
--- /dev/null
+++ b/flang/test/Transforms/function-attrs-readonly.fir
@@ -0,0 +1,90 @@
+// RUN: fir-opt --function-attr="set-readonly=true" %s | FileCheck %s --check-prefix=CHECK
+// RUN: fir-opt --function-attr="set-readonly=true set-nocapture=true set-noalias=true" %s | FileCheck %s --check-prefix=ALL
+// RUN: fir-opt --function-attr="set-readonly=false" %s | FileCheck %s --check-prefix=OFF
+
+// Test translation of the FIR marker placed on INTENT(IN) by-reference dummy
+// arguments to the LLVM dialect argument attribute.
+
+// CHECK-LABEL: func.func private @test_ref(
+// CHECK-SAME:    %{{.*}}: !fir.ref<i32> {fir.read_only, llvm.readonly}) {
+// ALL-LABEL:   func.func private @test_ref(
+// ALL-SAME:      %{{.*}}: !fir.ref<i32> {fir.read_only, llvm.noalias, llvm.nocapture, llvm.readonly}) {
+// OFF-LABEL:   func.func private @test_ref(
+// OFF-SAME:      %{{.*}}: !fir.ref<i32> {fir.read_only}) {
+func.func private @test_ref(%arg0: !fir.ref<i32> {fir.read_only}) {
+  return
+}
+
+// An unmarked reference is not read-only.
+// CHECK-LABEL: func.func private @test_unmarked_ref(
+// CHECK-SAME:    %{{.*}}: !fir.ref<i32>) {
+func.func private @test_unmarked_ref(%arg0: !fir.ref<i32>) {
+  return
+}
+
+// TARGET prevents noalias and nocapture, but it does not prevent readonly.
+// Match the complete argument attribute dictionary under the ALL run so an
+// accidental llvm.noalias or llvm.nocapture addition makes the test fail.
+// CHECK-LABEL: func.func private @test_target(
+// CHECK-SAME:    %{{.*}}: !fir.ref<i32> {fir.read_only, fir.target, llvm.readonly}) {
+// ALL-LABEL:   func.func private @test_target(
+// ALL-SAME:      %{{.*}}: !fir.ref<i32> {fir.read_only, fir.target, llvm.readonly}) {
+func.func private @test_target(%arg0: !fir.ref<i32> {fir.read_only, fir.target}) {
+  return
+}
+
+// FunctionAttr mechanically translates an explicit marker on any reference.
+// This is independent of which source-level dummies CallInterface marks.
+// CHECK-LABEL: func.func private @test_derived(
+// CHECK-SAME:    %{{.*}}: !fir.ref<!fir.type<_QFtest_derivedTt{p:!fir.box<!fir.ptr<i32>>}>> {fir.read_only, llvm.readonly}) {
+func.func private @test_derived(%arg0: !fir.ref<!fir.type<_QFtest_derivedTt{p:!fir.box<!fir.ptr<i32>>}>> {fir.read_only}) {
+  return
+}
+
+// On a reference to a descriptor, readonly protects the descriptor storage,
+// not the data address stored in the descriptor.
+// CHECK-LABEL: func.func private @test_pointer_descriptor(
+// CHECK-SAME:    %{{.*}}: !fir.ref<!fir.box<!fir.ptr<i32>>> {fir.read_only, llvm.readonly}) {
+// ALL-LABEL:   func.func private @test_pointer_descriptor(
+// ALL-SAME:      %{{.*}}: !fir.ref<!fir.box<!fir.ptr<i32>>> {fir.read_only, llvm.nocapture, llvm.readonly}) {
+func.func private @test_pointer_descriptor(%arg0: !fir.ref<!fir.box<!fir.ptr<i32>>> {fir.read_only}) {
+  return
+}
+
+// CHECK-LABEL: func.func private @test_allocatable_descriptor(
+// CHECK-SAME:    %{{.*}}: !fir.ref<!fir.box<!fir.heap<i32>>> {fir.read_only, llvm.readonly}) {
+func.func private @test_allocatable_descriptor(%arg0: !fir.ref<!fir.box<!fir.heap<i32>>> {fir.read_only}) {
+  return
+}
+
+// A Fortran BIND(C) definition must obey INTENT(IN), so readonly is valid.
+// Unlike ordinary definitions, it must not receive noalias or nocapture.
+// CHECK-LABEL: func.func @test_bindc_definition(
+// CHECK-SAME:    %{{.*}}: !fir.ref<i32> {fir.read_only, llvm.readonly}) attributes
+// ALL-LABEL:   func.func @test_bindc_definition(
+// ALL-SAME:      %{{.*}}: !fir.ref<i32> {fir.read_only, llvm.readonly}) attributes
+func.func @test_bindc_definition(%arg0: !fir.ref<i32> {fir.read_only}) attributes {fir.bindc_name = "test_bindc_definition", fir.proc_attrs = #fir.proc_attrs<bind_c>} {
+  return
+}
+
+// Do not apply readonly to a declaration of an external C procedure: its body
+// is not constrained by the Fortran INTENT appearing in the local interface.
+// Use a module-style FIR name so this exercises the BIND(C)-declaration check,
+// rather than the generic check that skips non-module declarations.
+// CHECK-LABEL: func.func private @_QMreadonly_modPbindc_declaration(
+// CHECK-SAME:    !fir.ref<i32> {fir.read_only}) attributes
+func.func private @_QMreadonly_modPbindc_declaration(!fir.ref<i32> {fir.read_only}) attributes {fir.bindc_name = "bindc_declaration", fir.proc_attrs = #fir.proc_attrs<bind_c>}
+
+// A declaration of a Fortran module procedure carries the Fortran contract.
+// CHECK-LABEL: func.func private @_QMreadonly_modPfortran_declaration(
+// CHECK-SAME:    !fir.ref<i32> {fir.read_only, llvm.readonly})
+func.func private @_QMreadonly_modPfortran_declaration(!fir.ref<i32> {fir.read_only})
+
+// A fir.box passed by value is not a ReferenceType, so FunctionAttr does not
+// translate its marker. This is distinct from POINTER and ALLOCATABLE dummies,
+// whose descriptors are passed by reference and are covered above.
+// CHECK-LABEL: func.func private @test_box(
+// CHECK-SAME:    %{{.*}}: !fir.box<!fir.array<?xi32>> {fir.read_only}) {
+func.func private @test_box(%arg0: !fir.box<!fir.array<?xi32>> {fir.read_only}) {
+  return
+}


        


More information about the flang-commits mailing list