[flang-commits] [flang] 4871338 - [flang][AArch64] Pass large BIND(C) VALUE derived types indirectly (#215508)

via flang-commits flang-commits at lists.llvm.org
Thu Aug 13 00:09:11 PDT 2026


Author: Kareem Ergawy
Date: 2026-08-13T09:09:06+02:00
New Revision: 487133883a292655bbee36e00fff5bbd0c5223f9

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

LOG: [flang][AArch64] Pass large BIND(C) VALUE derived types indirectly (#215508)

AAPCS64 "Parameter passing rules" B.4 states: "If the argument type is a
Composite Type that is larger than 16 bytes, then the argument is copied
to memory allocated by the caller and the argument is replaced by a
pointer to the copy." (see:
https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst#parameter-passing-rules)

Flang instead marked such an argument byval, which lets the target lower
it as a by-value aggregate placed in the argument stack area, so it
consumed no register. A C function that follows the standard expects a
pointer in that register, so the arguments no longer lined up and the
callee read the wrong value.

Add an indirect attribute to the argument marshalling and use it on
AArch64 for records that are too large to be passed in registers. Like
byval it makes the caller copy the value into a temporary and pass its
address, but it does not attach the byval attribute to the argument.
Records of 16 bytes or less, records passed on the stack because no
register is left, and records returned by value are unchanged.

Co-Authored-By: Claude

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply at anthropic.com>

Added: 
    flang/test/Fir/struct-passing-aarch64-indirect.fir

Modified: 
    flang/include/flang/Optimizer/CodeGen/Target.h
    flang/lib/Optimizer/CodeGen/Target.cpp
    flang/lib/Optimizer/CodeGen/TargetRewrite.cpp
    flang/test/Fir/struct-passing-aarch64-byval.fir

Removed: 
    


################################################################################
diff  --git a/flang/include/flang/Optimizer/CodeGen/Target.h b/flang/include/flang/Optimizer/CodeGen/Target.h
index 2a374875d31a0..10eb786776a2f 100644
--- a/flang/include/flang/Optimizer/CodeGen/Target.h
+++ b/flang/include/flang/Optimizer/CodeGen/Target.h
@@ -39,14 +39,23 @@ class Attributes {
 
   Attributes(unsigned short alignment = 0, bool byval = false,
              bool sret = false, bool append = false,
-             IntegerExtension intExt = IntegerExtension::None)
+             IntegerExtension intExt = IntegerExtension::None,
+             bool indirect = false)
       : alignment{alignment}, byval{byval}, sret{sret}, append{append},
-        intExt{intExt} {}
+        indirect{indirect}, intExt{intExt} {}
 
   unsigned getAlignment() const { return alignment; }
   bool hasAlignment() const { return alignment != 0; }
   bool isByVal() const { return byval; }
   bool isSRet() const { return sret; }
+  /// The argument is passed indirectly: the caller materializes a copy of the
+  /// aggregate and passes the address of that copy, which the ABI assigns to a
+  /// register like any other pointer. Like `byval` this requires the caller to
+  /// make a copy, but the pointer itself is the argument, so no `llvm.byval`
+  /// attribute is attached and the target does not lower it as a by-value
+  /// aggregate. Some ABIs (e.g. AAPCS64) require this form for aggregates that
+  /// are too large to be passed in registers.
+  bool isIndirect() const { return indirect; }
   bool isAppend() const { return append; }
   bool isZeroExt() const { return intExt == IntegerExtension::Zero; }
   bool isSignExt() const { return intExt == IntegerExtension::Sign; }
@@ -57,6 +66,7 @@ class Attributes {
   bool byval : 1;
   bool sret : 1;
   bool append : 1;
+  bool indirect : 1;
   IntegerExtension intExt;
 };
 

diff  --git a/flang/lib/Optimizer/CodeGen/Target.cpp b/flang/lib/Optimizer/CodeGen/Target.cpp
index 32c403de3ac39..acbf0edcde5a6 100644
--- a/flang/lib/Optimizer/CodeGen/Target.cpp
+++ b/flang/lib/Optimizer/CodeGen/Target.cpp
@@ -1008,13 +1008,39 @@ struct TargetAArch64 : public GenericTarget<TargetAArch64> {
     return marshal;
   }
 
+  /// AAPCS64 "Parameter passing rules" B.4: "If the argument type is a
+  /// Composite Type that is larger than 16 bytes, then the argument is copied
+  /// to memory allocated by the caller and the argument is replaced by a
+  /// pointer to the copy." Marking such an argument `byval` instead would let
+  /// the target lower it as a by-value aggregate, which does not match this
+  /// rule.
+  CodeGenSpecifics::Marshalling passIndirectly(mlir::Location loc,
+                                               mlir::Type ty) const {
+    CodeGenSpecifics::Marshalling marshal;
+    auto sizeAndAlign =
+        fir::getTypeSizeAndAlignmentOrCrash(loc, ty, getDataLayout(), kindMap);
+    // The pointee is the caller's copy, so the alignment reported for the
+    // pointer is the natural alignment of the aggregate.
+    marshal.emplace_back(
+        fir::ReferenceType::get(ty),
+        AT{sizeAndAlign.second, /*byval=*/false, /*sret=*/false,
+           /*append=*/false, /*intExt=*/AT::IntegerExtension::None,
+           /*indirect=*/true});
+    return marshal;
+  }
+
   CodeGenSpecifics::Marshalling
   structType(mlir::Location loc, fir::RecordType type, bool isResult) const {
     NRegs nregs = usedRegsForRecordType(loc, type);
 
-    // If the type needs no registers it must need to be passed on the stack
-    if (nregs.n == 0)
-      return passOnTheStack(loc, type, isResult);
+    // If the type needs no registers it is too large to be passed in
+    // registers. As an argument it is passed indirectly; as a result it is
+    // returned via the caller-provided indirect result buffer (sret).
+    if (nregs.n == 0) {
+      if (isResult)
+        return passOnTheStack(loc, type, /*isResult=*/true);
+      return passIndirectly(loc, type);
+    }
 
     CodeGenSpecifics::Marshalling marshal;
 

diff  --git a/flang/lib/Optimizer/CodeGen/TargetRewrite.cpp b/flang/lib/Optimizer/CodeGen/TargetRewrite.cpp
index 22435aafe352d..8070877554ab1 100644
--- a/flang/lib/Optimizer/CodeGen/TargetRewrite.cpp
+++ b/flang/lib/Optimizer/CodeGen/TargetRewrite.cpp
@@ -252,7 +252,10 @@ class TargetRewrite : public fir::impl::TargetRewritePassBase<TargetRewrite> {
     // We are going to generate an alloca, so save the stack pointer.
     if (!savedStackPtr)
       savedStackPtr = genStackSave(loc);
-    if (attr.isByVal()) {
+    if (attr.isByVal() || attr.isIndirect()) {
+      // Both forms require the caller to materialize a copy of the value and
+      // pass the address of that copy. They 
diff er only in the attribute that
+      // is attached to the argument in the signature.
       mlir::Value mem = fir::AllocaOp::create(*rewriter, loc, oldType);
       fir::StoreOp::create(*rewriter, loc, oper, mem);
       if (mem.getType() != resTy)
@@ -370,11 +373,11 @@ class TargetRewrite : public fir::impl::TargetRewritePassBase<TargetRewrite> {
                         newInTyAndAttrs);
   }
 
-  static bool hasByValOrSRetArgs(
+  static bool hasAbiArgAttributes(
       const fir::CodeGenSpecifics::Marshalling &newInTyAndAttrs) {
     return llvm::any_of(newInTyAndAttrs, [](auto arg) {
       const auto &attr = std::get<fir::CodeGenSpecifics::Attributes>(arg);
-      return attr.isByVal() || attr.isSRet();
+      return attr.isByVal() || attr.isSRet() || attr.isIndirect();
     });
   }
 
@@ -570,7 +573,7 @@ class TargetRewrite : public fir::impl::TargetRewritePassBase<TargetRewrite> {
       // Always set ABI argument attributes on call operations, even when
       // direct, as required by
       // https://llvm.org/docs/LangRef.html#parameter-attributes.
-      if (hasByValOrSRetArgs(newInTyAndAttrs)) {
+      if (hasAbiArgAttributes(newInTyAndAttrs)) {
         llvm::SmallVector<mlir::Attribute> argAttrsArray;
         for (const auto &arg :
              llvm::ArrayRef<fir::CodeGenSpecifics::TypeAndAttr>(newInTyAndAttrs)
@@ -1307,27 +1310,28 @@ class TargetRewrite : public fir::impl::TargetRewritePassBase<TargetRewrite> {
       auto index = e.index();
       auto attr = std::get<fir::CodeGenSpecifics::Attributes>(tup);
       auto argNo = newInTyAndAttrs.size();
-      if (attr.isByVal()) {
-        if (auto align = attr.getAlignment())
-          fixups.emplace_back(FixupTy::Codes::ArgumentAsLoad, argNo,
-                              [=](OpTy func) {
+      if (attr.isByVal() || attr.isIndirect()) {
+        // In both cases the callee receives a pointer to a copy made by the
+        // caller and loads the value from it. They 
diff er only in the attribute
+        // attached to the argument: `byval` marks it as a by-value aggregate
+        // for the target to lower, whereas an indirect argument is passed as a
+        // plain pointer.
+        const bool setByVal = attr.isByVal();
+        const unsigned align = attr.getAlignment();
+        fixups.emplace_back(FixupTy::Codes::ArgumentAsLoad, argNo,
+                            [=](OpTy func) {
+                              if (setByVal) {
                                 auto elemType = fir::dyn_cast_ptrOrBoxEleTy(
                                     func.getFunctionType().getInput(argNo));
                                 func.setArgAttr(argNo, "llvm.byval",
                                                 mlir::TypeAttr::get(elemType));
+                              }
+                              if (align)
                                 func.setArgAttr(
                                     argNo, "llvm.align",
                                     rewriter->getIntegerAttr(
                                         rewriter->getIntegerType(32), align));
-                              });
-        else
-          fixups.emplace_back(FixupTy::Codes::ArgumentAsLoad,
-                              newInTyAndAttrs.size(), [=](OpTy func) {
-                                auto elemType = fir::dyn_cast_ptrOrBoxEleTy(
-                                    func.getFunctionType().getInput(argNo));
-                                func.setArgAttr(argNo, "llvm.byval",
-                                                mlir::TypeAttr::get(elemType));
-                              });
+                            });
       } else {
         if (auto align = attr.getAlignment())
           fixups.emplace_back(

diff  --git a/flang/test/Fir/struct-passing-aarch64-byval.fir b/flang/test/Fir/struct-passing-aarch64-byval.fir
index 087efba393014..c2e41c890caaa 100644
--- a/flang/test/Fir/struct-passing-aarch64-byval.fir
+++ b/flang/test/Fir/struct-passing-aarch64-byval.fir
@@ -69,9 +69,20 @@ func.func private @too_many_hfa(!fir.type<hfa_max{i:f128,j:f128,k:f128,l:f128}>,
                            !fir.type<hfa_max{i:f128,j:f128,k:f128,l:f128}>,
                            !fir.type<hfa_max{i:f128,j:f128,k:f128,l:f128}>)
 
-// CHECK-LABEL: func.func private @too_big(!fir.ref<!fir.type<too_big{i:!fir.array<5xi32>}>> {{{.*}}, llvm.byval = !fir.type<too_big{i:!fir.array<5xi32>}>})
+// A composite larger than 16 bytes is passed indirectly: the caller makes a
+// copy and passes a pointer to it in a general-purpose register (AAPCS64 B.4).
+// It is not marked byval, which would instead let the target lower it as a
+// by-value aggregate and consume no register.
+// CHECK-LABEL: func.func private @too_big(!fir.ref<!fir.type<too_big{i:!fir.array<5xi32>}>> {llvm.align = 4 : i32})
 func.func private @too_big(!fir.type<too_big{i:!fir.array<5xi32>}>)
 
+// An indirectly passed composite still consumes a register, so the arguments
+// that follow it keep their register assignment.
+// CHECK-LABEL: func.func private @big_among_scalars(!fir.ref<i64>, i32,
+// CHECK-SAME: !fir.ref<!fir.type<big_char{i:!fir.array<128x!fir.char<1>>}>> {llvm.align = 1 : i32}, i32)
+func.func private @big_among_scalars(!fir.ref<i64>, i32,
+                       !fir.type<big_char{i:!fir.array<128x!fir.char<1>>}>, i32)
+
 // CHECK-LABEL: func.func private @pointer_type(!fir.ref<i64>, !fir.array<1xi64>)
 func.func private @pointer_type(!fir.ref<i64>, !fir.type<pointer_type{i:i64}>)
 

diff  --git a/flang/test/Fir/struct-passing-aarch64-indirect.fir b/flang/test/Fir/struct-passing-aarch64-indirect.fir
new file mode 100644
index 0000000000000..63f55e45e1fac
--- /dev/null
+++ b/flang/test/Fir/struct-passing-aarch64-indirect.fir
@@ -0,0 +1,57 @@
+/// Test the AArch64 ABI rewrite of a BIND(C), VALUE derived type argument that
+/// is larger than 16 bytes. Such an argument is passed as a pointer to a copy
+/// made by the caller, and is not marked byval.
+
+// RUN: fir-opt --target-rewrite="target=aarch64-unknown-linux-gnu" %s | FileCheck %s
+
+!big = !fir.type<big{i:!fir.array<128x!fir.char<1>>}>
+
+/// The callee receives a pointer and loads the value from it.
+// CHECK-LABEL: func.func @callee(
+// CHECK-SAME:      %[[ARG:.*]]: !fir.ref<!fir.type<big{i:!fir.array<128x!fir.char<1>>}>> {llvm.align = 1 : i32}) {
+// CHECK-NOT:     llvm.byval
+// CHECK:         %[[REF:.*]] = fir.convert %[[ARG]]
+// CHECK:         fir.load %[[REF]]
+func.func @callee(%arg0: !big) {
+  return
+}
+
+/// The caller stores the value into a temporary and passes its address.
+// CHECK-LABEL: func.func @caller(
+// CHECK-NOT:     llvm.byval
+// CHECK:         %[[VAL:.*]] = fir.load
+// CHECK:         %[[TMP:.*]] = fir.alloca !fir.type<big{i:!fir.array<128x!fir.char<1>>}>
+// CHECK:         fir.store %[[VAL]] to %[[TMP]]
+// CHECK:         fir.call @callee(%[[TMP]]) : (!fir.ref<!fir.type<big{i:!fir.array<128x!fir.char<1>>}>> {llvm.align = 1 : i32}) -> ()
+func.func @caller(%arg0: !fir.ref<!big>) {
+  %0 = fir.load %arg0 : !fir.ref<!big>
+  fir.call @callee(%0) : (!big) -> ()
+  return
+}
+
+/// The same rewrite for a record whose natural alignment is not 1. The pointer
+/// argument carries the alignment of the aggregate it points at, so this one is
+/// 8 rather than the 1 above.
+
+!bigreal = !fir.type<bigreal{i:!fir.array<32xf64>}>
+
+// CHECK-LABEL: func.func @callee_real(
+// CHECK-SAME:      %[[ARG:.*]]: !fir.ref<!fir.type<bigreal{i:!fir.array<32xf64>}>> {llvm.align = 8 : i32}) {
+// CHECK-NOT:     llvm.byval
+// CHECK:         %[[REF:.*]] = fir.convert %[[ARG]]
+// CHECK:         fir.load %[[REF]]
+func.func @callee_real(%arg0: !bigreal) {
+  return
+}
+
+// CHECK-LABEL: func.func @caller_real(
+// CHECK-NOT:     llvm.byval
+// CHECK:         %[[VAL:.*]] = fir.load
+// CHECK:         %[[TMP:.*]] = fir.alloca !fir.type<bigreal{i:!fir.array<32xf64>}>
+// CHECK:         fir.store %[[VAL]] to %[[TMP]]
+// CHECK:         fir.call @callee_real(%[[TMP]]) : (!fir.ref<!fir.type<bigreal{i:!fir.array<32xf64>}>> {llvm.align = 8 : i32}) -> ()
+func.func @caller_real(%arg0: !fir.ref<!bigreal>) {
+  %0 = fir.load %arg0 : !fir.ref<!bigreal>
+  fir.call @callee_real(%0) : (!bigreal) -> ()
+  return
+}


        


More information about the flang-commits mailing list