[flang-commits] [flang] [flang] Fix `getTypeSizeAndAlignment` for packed/tail-padded `RecordType` and `TRANSFER` gate (PR #220377)

Daniel Chen via flang-commits flang-commits at lists.llvm.org
Thu Sep 3 18:02:35 PDT 2026


https://github.com/DanielCChen updated https://github.com/llvm/llvm-project/pull/220377

>From 1cb6fea4e527fd2021e78f79d59d44dc965524b0 Mon Sep 17 00:00:00 2001
From: Daniel Chen <cdchen at ca.ibm.com>
Date: Thu, 3 Sep 2026 17:22:35 -0400
Subject: [PATCH 1/2] [flang] Fix getTypeSizeAndAlignment for RecordType

Handle packed records and tail padding correctly when computing allocation extents. Keep the stored-representation width used by TRANSFER distinct from allocation size, and use aligned temporary storage when a record source is less aligned than the trivial result type.

Update affected callers and tests, including FIR box element sizing, allocation placement, CUDA device-global and shared-memory sizing, PPC64le and SystemZ argument passing, and TRANSFER lowering.
---
 .../include/flang/Optimizer/Dialect/FIRType.h | 44 ++++++++++-
 flang/lib/Optimizer/Builder/IntrinsicCall.cpp | 64 +++++++++++++---
 flang/lib/Optimizer/Dialect/FIROps.cpp        |  8 +-
 flang/lib/Optimizer/Dialect/FIRType.cpp       | 40 ++++++++--
 flang/test/Fir/CUDA/cuda-constructor-2.f90    | 62 ++++++++++++++++
 flang/test/Fir/CUDA/cuda-shared-offset.mlir   | 44 +++++++++++
 flang/test/Fir/box-elesize-canonicalize.fir   | 63 ++++++++++++++++
 .../test/Fir/struct-passing-ppc64le-byval.fir |  8 ++
 .../Intrinsics/transfer-rec-tail-pad.f90      | 74 +++++++++++++++++++
 .../test/Transforms/allocation-placement.fir  | 44 +++++++++++
 10 files changed, 426 insertions(+), 25 deletions(-)
 create mode 100644 flang/test/Lower/Intrinsics/transfer-rec-tail-pad.f90

diff --git a/flang/include/flang/Optimizer/Dialect/FIRType.h b/flang/include/flang/Optimizer/Dialect/FIRType.h
index 6684af86a33fa..a01e6ddcb5c97 100644
--- a/flang/include/flang/Optimizer/Dialect/FIRType.h
+++ b/flang/include/flang/Optimizer/Dialect/FIRType.h
@@ -556,6 +556,41 @@ std::string getTypeAsString(mlir::Type ty, const KindMapping &kindMap,
                             llvm::StringRef prefix = "");
 
 /// Return the size and alignment of FIR types.
+///
+/// The returned size and the effect of \p storeSizeOnly differ by type
+/// category:
+///
+/// - **Trivial scalars** (integer, real, complex, logical, character):
+///   size = dl.getTypeSize(), which is the *store* size (data bytes only,
+///   no tail padding).  For example, x86 f80 has store size 10 and ABI
+///   alignment 16; the returned size is 10, not 16.  Callers that need
+///   the allocation size (e.g. FoldBoxEleSize, array element strides)
+///   must round up: llvm::alignTo(size, alignment).  storeSizeOnly has
+///   no effect on this category.
+///
+/// - **Sequences (fir::SequenceType)**:
+///   Each element size is rounded to its alignment boundary before
+///   multiplying by the element count (allocation-size stride).  This
+///   rounding is applied regardless of storeSizeOnly, so the result
+///   includes per-element allocation padding.
+///
+/// - **Packed records (fir::RecordType with isPacked)**:
+///   Fields are laid out back-to-back using each field's allocation size
+///   (llvm::alignTo(fieldStoreSize, fieldAlign)), matching LLVM's packed
+///   StructLayout.  No inter-field or tail padding is added.  storeSizeOnly
+///   has no effect on this category.
+///
+/// - **Unpacked records (fir::RecordType, not packed)**:
+///   Fields are laid out with inter-field alignment padding; each field
+///   occupies llvm::alignTo(fieldSize, fieldAlign) bytes.  By default
+///   (storeSizeOnly=false) the total size is rounded up to the record's
+///   own alignment, giving the allocation size (tail-padded).  When
+///   storeSizeOnly=true, that final tail-rounding is skipped, returning
+///   the sum of field allocation sizes without outer tail padding.  This
+///   is the *only* category where storeSizeOnly has an effect.  Use
+///   storeSizeOnly=true when comparing Fortran STORAGE_SIZE data-bit
+///   widths (e.g. the TRANSFER inline gate).
+///
 /// TODO: consider moving this to a DataLayoutTypeInterface implementation
 /// for FIR types. It should first be ensured that it is OK to open the gate of
 /// target dependent type size inquiries in lowering. It would also not be
@@ -569,10 +604,11 @@ getTypeSizeAndAlignmentOrCrash(mlir::Location loc, mlir::Type ty,
                                const fir::KindMapping &kindMap);
 
 /// This variant returns std::nullopt if an unsupported type is passed.
-std::optional<std::pair<uint64_t, unsigned short>>
-getTypeSizeAndAlignment(mlir::Location loc, mlir::Type ty,
-                        const mlir::DataLayout &dl,
-                        const fir::KindMapping &kindMap);
+/// See the block comment above for per-category size and storeSizeOnly
+/// semantics.
+std::optional<std::pair<uint64_t, unsigned short>> getTypeSizeAndAlignment(
+    mlir::Location loc, mlir::Type ty, const mlir::DataLayout &dl,
+    const fir::KindMapping &kindMap, bool storeSizeOnly = false);
 
 } // namespace fir
 
diff --git a/flang/lib/Optimizer/Builder/IntrinsicCall.cpp b/flang/lib/Optimizer/Builder/IntrinsicCall.cpp
index a139af9a29e57..6a71a3d9a14b9 100644
--- a/flang/lib/Optimizer/Builder/IntrinsicCall.cpp
+++ b/flang/lib/Optimizer/Builder/IntrinsicCall.cpp
@@ -8972,22 +8972,64 @@ IntrinsicLibrary::genTransfer(mlir::Type resultType,
         (fir::isa_trivial(sourceType) ||
          mlir::isa<fir::RecordType>(sourceType)) &&
         fir::isa_trivial(moldType)) {
+      // Compare store sizes (data bytes, no tail padding) because Fortran
+      // TRANSFER is defined over STORAGE_SIZE data bits.  For a RecordType
+      // source, storeSizeOnly=true strips the tail padding that the compiler
+      // adds for alignment but that is not part of the type's data.
+      // Note: the runtime (TransferImpl) copies source.ElementBytes() bytes,
+      // which is the allocation size and may include tail padding.  The store-
+      // size comparison here is therefore *not* about matching the runtime's
+      // copy width; it is about selecting the correct inline path according to
+      // the standard's data-bit definition.
+      //
+      // Alignment is a separate concern: when the source alignment is less
+      // than the result type's alignment, the RecordType path below copies
+      // into a result-aligned alloca rather than loading directly from the
+      // source pointer.
       auto sourceSizeAndAlign = fir::getTypeSizeAndAlignment(
-          loc, sourceType, builder.getDataLayout(), builder.getKindMap());
+          loc, sourceType, builder.getDataLayout(), builder.getKindMap(),
+          /*storeSizeOnly=*/true);
       auto resultSizeAndAlign = fir::getTypeSizeAndAlignment(
-          loc, resultType, builder.getDataLayout(), builder.getKindMap());
+          loc, resultType, builder.getDataLayout(), builder.getKindMap(),
+          /*storeSizeOnly=*/true);
       if (sourceSizeAndAlign && resultSizeAndAlign &&
           sourceSizeAndAlign->first == resultSizeAndAlign->first) {
-        if (sourceType.isSignlessIntOrFloat() &&
-            resultType.isSignlessIntOrFloat()) {
-          mlir::Value val = fir::LoadOp::create(builder, loc, sourceBase);
-          if (sourceType != resultType)
-            val = mlir::arith::BitcastOp::create(builder, loc, resultType, val);
-          return val;
+        if (fir::isa_trivial(sourceType)) {
+          // Both source and result are trivial scalars of the same store
+          // size.  Use arith.bitcast for signless integer/float pairs;
+          // for other trivial types (e.g. unsigned integers) arith.bitcast
+          // is not available, so cast the source address and load.
+          if (sourceType.isSignlessIntOrFloat() &&
+              resultType.isSignlessIntOrFloat()) {
+            mlir::Value val = fir::LoadOp::create(builder, loc, sourceBase);
+            if (sourceType != resultType)
+              val =
+                  mlir::arith::BitcastOp::create(builder, loc, resultType, val);
+            return val;
+          }
+          mlir::Type refTy = builder.getRefType(resultType);
+          mlir::Value cast = builder.createConvert(loc, refTy, sourceBase);
+          return fir::LoadOp::create(builder, loc, cast);
+        }
+        // The source is a RecordType.  When the source ABI alignment is
+        // sufficient for resultType, a direct address cast and load is safe.
+        // When sourceAlign < resultAlign (e.g. {i64,i16} is 8-byte aligned
+        // while x86_fp80 requires 16-byte alignment), loading resultType
+        // directly from sourceBase would claim an over-aligned address,
+        // producing undefined behaviour.  In that case copy the bytes into
+        // a result-typed alloca (which receives resultType's natural
+        // alignment) and load from there.
+        if (sourceSizeAndAlign->second >= resultSizeAndAlign->second) {
+          mlir::Type refTy = builder.getRefType(resultType);
+          mlir::Value cast = builder.createConvert(loc, refTy, sourceBase);
+          return fir::LoadOp::create(builder, loc, cast);
         }
-        mlir::Type refTy = builder.getRefType(resultType);
-        mlir::Value cast = builder.createConvert(loc, refTy, sourceBase);
-        return fir::LoadOp::create(builder, loc, cast);
+        mlir::Value tmp = fir::AllocaOp::create(builder, loc, resultType);
+        mlir::Value srcLoad = fir::LoadOp::create(builder, loc, sourceBase);
+        mlir::Type srcRefTy = builder.getRefType(sourceType);
+        mlir::Value tmpAsSrc = builder.createConvert(loc, srcRefTy, tmp);
+        fir::StoreOp::create(builder, loc, srcLoad, tmpAsSrc);
+        return fir::LoadOp::create(builder, loc, tmp);
       }
     }
   }
diff --git a/flang/lib/Optimizer/Dialect/FIROps.cpp b/flang/lib/Optimizer/Dialect/FIROps.cpp
index 0aa5fd60e2b52..f6cfb16acece6 100644
--- a/flang/lib/Optimizer/Dialect/FIROps.cpp
+++ b/flang/lib/Optimizer/Dialect/FIROps.cpp
@@ -1380,9 +1380,11 @@ struct FoldBoxEleSize : public mlir::OpRewritePattern<fir::BoxEleSizeOp> {
     if (!sizeAndAlign)
       return mlir::failure();
 
-    // The descriptor stores the byte stride between elements (not the raw
-    // natural size), so we must round up to alignment just as
-    // fir::computeElementDistance does.
+    // The descriptor stores the byte stride between elements, which is the
+    // allocation size including tail padding.  getTypeSizeAndAlignment already
+    // rounds up to alignment for RecordType, so llvm::alignTo here is a no-op
+    // for struct types; it is kept for scalar types (e.g. x86_fp80) where the
+    // rounding is still needed, matching fir::computeElementDistance.
     auto [size, alignment] = *sizeAndAlign;
     std::int64_t distance = llvm::alignTo(size, alignment);
 
diff --git a/flang/lib/Optimizer/Dialect/FIRType.cpp b/flang/lib/Optimizer/Dialect/FIRType.cpp
index 178860239e17d..6a9fe55ca3869 100644
--- a/flang/lib/Optimizer/Dialect/FIRType.cpp
+++ b/flang/lib/Optimizer/Dialect/FIRType.cpp
@@ -1638,10 +1638,9 @@ void FIROpsDialect::registerTypes() {
       OpenMPPointerLikeModel<fir::LLVMPointerType>>(*getContext());
 }
 
-std::optional<std::pair<uint64_t, unsigned short>>
-fir::getTypeSizeAndAlignment(mlir::Location loc, mlir::Type ty,
-                             const mlir::DataLayout &dl,
-                             const fir::KindMapping &kindMap) {
+std::optional<std::pair<uint64_t, unsigned short>> fir::getTypeSizeAndAlignment(
+    mlir::Location loc, mlir::Type ty, const mlir::DataLayout &dl,
+    const fir::KindMapping &kindMap, bool storeSizeOnly) {
   if (ty.isIntOrIndexOrFloat() ||
       mlir::isa<mlir::ComplexType, mlir::VectorType,
                 mlir::DataLayoutTypeInterface>(ty)) {
@@ -1650,7 +1649,8 @@ fir::getTypeSizeAndAlignment(mlir::Location loc, mlir::Type ty,
     return std::pair{size, alignment};
   }
   if (auto seqTy = mlir::dyn_cast<fir::SequenceType>(ty)) {
-    auto result = getTypeSizeAndAlignment(loc, seqTy.getEleTy(), dl, kindMap);
+    auto result = getTypeSizeAndAlignment(loc, seqTy.getEleTy(), dl, kindMap,
+                                          storeSizeOnly);
     if (!result)
       return result;
     auto [eleSize, eleAlign] = *result;
@@ -1661,8 +1661,29 @@ fir::getTypeSizeAndAlignment(mlir::Location loc, mlir::Type ty,
   if (auto recTy = mlir::dyn_cast<fir::RecordType>(ty)) {
     std::uint64_t size = 0;
     unsigned short align = 1;
+    if (recTy.isPacked()) {
+      // LLVM packed structs (<{ ... }>) place fields back-to-back with no
+      // inter-field alignment padding and no tail padding.  Each component
+      // still occupies its allocation size (llvm::alignTo(storeSize, ABI
+      // alignment)), because LLVM's packed StructLayout advances by
+      // getTypeAllocSize, not getTypeStoreSize.  For example, x86 f80 has
+      // store size 10 bytes but ABI alignment 16 bytes, so its allocation
+      // size is 16 bytes; a packed {f80, i8} therefore occupies 17 bytes,
+      // not 11.  The packed struct's own ABI alignment is always 1.
+      for (auto component : recTy.getTypeList()) {
+        auto result =
+            getTypeSizeAndAlignment(loc, component.second, dl, kindMap);
+        if (!result)
+          return result;
+        auto [compSize, compAlign] = *result;
+        size += llvm::alignTo(compSize, compAlign); // allocation size per field
+      }
+      // Packed structs have no tail padding regardless of storeSizeOnly.
+      return std::pair{size, static_cast<unsigned short>(1)};
+    }
     for (auto component : recTy.getTypeList()) {
-      auto result = getTypeSizeAndAlignment(loc, component.second, dl, kindMap);
+      auto result = getTypeSizeAndAlignment(loc, component.second, dl, kindMap,
+                                            storeSizeOnly);
       if (!result)
         return result;
       auto [compSize, compAlign] = *result;
@@ -1670,6 +1691,10 @@ fir::getTypeSizeAndAlignment(mlir::Location loc, mlir::Type ty,
           llvm::alignTo(size, compAlign) + llvm::alignTo(compSize, compAlign);
       align = std::max(align, compAlign);
     }
+    // Include tail padding so the size matches the allocation size,
+    // unless the caller only wants the store size (no tail padding).
+    if (!storeSizeOnly)
+      size = llvm::alignTo(size, align);
     return std::pair{size, align};
   }
   if (auto logical = mlir::dyn_cast<fir::LogicalType>(ty)) {
@@ -1681,7 +1706,8 @@ fir::getTypeSizeAndAlignment(mlir::Location loc, mlir::Type ty,
     mlir::Type intTy = mlir::IntegerType::get(
         character.getContext(),
         kindMap.getCharacterBitsize(character.getFKind()));
-    auto result = getTypeSizeAndAlignment(loc, intTy, dl, kindMap);
+    auto result =
+        getTypeSizeAndAlignment(loc, intTy, dl, kindMap, storeSizeOnly);
     if (!result)
       return result;
     auto [compSize, compAlign] = *result;
diff --git a/flang/test/Fir/CUDA/cuda-constructor-2.f90 b/flang/test/Fir/CUDA/cuda-constructor-2.f90
index e93a721a192f0..b97a9aee0eba8 100644
--- a/flang/test/Fir/CUDA/cuda-constructor-2.f90
+++ b/flang/test/Fir/CUDA/cuda-constructor-2.f90
@@ -351,3 +351,65 @@ module attributes {dlti.dl_spec = #dlti.dl_spec<i8 = dense<8> : vector<2xi64>, i
 // CHECK-NOT: cuf.register_module
 // CHECK-NOT: fir.call @_FortranACUFRegisterVariable
 // CHECK: llvm.mlir.global_ctors ctors = [@__cudaFortranConstructor]
+
+// -----
+
+// Tail-padded device global: a record type {i32, i8} has 5 typed bytes but
+// 8 allocation bytes (3 bytes of tail padding for i32 alignment).
+// CUFAddConstructor must register the full allocation size (8), not the raw
+// typed size (5), so the CUDA runtime maps the correct number of bytes.
+
+module attributes {dlti.dl_spec = #dlti.dl_spec<#dlti.dl_entry<!llvm.ptr, dense<64> : vector<4xi64>>, #dlti.dl_entry<!llvm.ptr<271>, dense<32> : vector<4xi64>>, #dlti.dl_entry<!llvm.ptr<270>, dense<32> : vector<4xi64>>, #dlti.dl_entry<f128, dense<128> : vector<2xi64>>, #dlti.dl_entry<f64, dense<64> : vector<2xi64>>, #dlti.dl_entry<f80, dense<128> : vector<2xi64>>, #dlti.dl_entry<f16, dense<16> : vector<2xi64>>, #dlti.dl_entry<i32, dense<32> : vector<2xi64>>, #dlti.dl_entry<i16, dense<16> : vector<2xi64>>, #dlti.dl_entry<i128, dense<128> : vector<2xi64>>, #dlti.dl_entry<i8, dense<8> : vector<2xi64>>, #dlti.dl_entry<!llvm.ptr<272>, dense<64> : vector<4xi64>>, #dlti.dl_entry<i64, dense<64> : vector<2xi64>>, #dlti.dl_entry<i1, dense<8> : vector<2xi64>>, #dlti.dl_entry<"dlti.endianness", "little">, #dlti.dl_entry<"dlti.stack_alignment", 128 : i64>>, fir.defaultkind = "a1c4d8i4l4r4", fir.kindmap = "", gpu.container_module, llvm.data_layout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", llvm.target_triple = "x86_64-unknown-linux-gnu"} {
+
+  fir.global @_QMtestEtp_dev {data_attr = #cuf.cuda<device>} : !fir.type<tp{a:i32,b:i8}> {
+    %0 = fir.zero_bits !fir.type<tp{a:i32,b:i8}>
+    fir.has_value %0 : !fir.type<tp{a:i32,b:i8}>
+  }
+
+  gpu.module @cuda_device_mod {
+    gpu.func @_QMtestPkernel() kernel {
+      gpu.return
+    }
+    fir.global @_QMtestEtp_dev {data_attr = #cuf.cuda<device>} : !fir.type<tp{a:i32,b:i8}> {
+      %0 = fir.zero_bits !fir.type<tp{a:i32,b:i8}>
+      fir.has_value %0 : !fir.type<tp{a:i32,b:i8}>
+    }
+  }
+}
+
+// Registered size must be 8 (allocation size including tail padding), not 5
+// (raw typed size).  A wrong value of 5 would cause the CUDA runtime to map
+// too few bytes and leave the 3 tail-padding bytes unmapped.
+// NOUNIFIED: arith.constant 8 : index
+// NOUNIFIED: fir.call @_FortranACUFRegisterVariable
+// UNIFIED: cuf.register_variable_static @_QMtestEtp_dev("_QMtestEtp_dev", 8) {deviceResident}
+
+// -----
+
+// Packed device global: a packed record type <{i32, f64}> has no alignment
+// gaps and no tail padding; its size is 4+8=12 bytes (not 16, which would be
+// the aligned size of an unpacked {i32, f64}).
+// CUFAddConstructor must register size 12, not 16.
+
+module attributes {dlti.dl_spec = #dlti.dl_spec<#dlti.dl_entry<!llvm.ptr, dense<64> : vector<4xi64>>, #dlti.dl_entry<!llvm.ptr<271>, dense<32> : vector<4xi64>>, #dlti.dl_entry<!llvm.ptr<270>, dense<32> : vector<4xi64>>, #dlti.dl_entry<f128, dense<128> : vector<2xi64>>, #dlti.dl_entry<f64, dense<64> : vector<2xi64>>, #dlti.dl_entry<f80, dense<128> : vector<2xi64>>, #dlti.dl_entry<f16, dense<16> : vector<2xi64>>, #dlti.dl_entry<i32, dense<32> : vector<2xi64>>, #dlti.dl_entry<i16, dense<16> : vector<2xi64>>, #dlti.dl_entry<i128, dense<128> : vector<2xi64>>, #dlti.dl_entry<i8, dense<8> : vector<2xi64>>, #dlti.dl_entry<!llvm.ptr<272>, dense<64> : vector<4xi64>>, #dlti.dl_entry<i64, dense<64> : vector<2xi64>>, #dlti.dl_entry<i1, dense<8> : vector<2xi64>>, #dlti.dl_entry<"dlti.endianness", "little">, #dlti.dl_entry<"dlti.stack_alignment", 128 : i64>>, fir.defaultkind = "a1c4d8i4l4r4", fir.kindmap = "", gpu.container_module, llvm.data_layout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", llvm.target_triple = "x86_64-unknown-linux-gnu"} {
+
+  fir.global @_QMtestEtp_packed_dev {data_attr = #cuf.cuda<device>} : !fir.type<tp<{i:i32,d:f64}>> {
+    %0 = fir.zero_bits !fir.type<tp<{i:i32,d:f64}>>
+    fir.has_value %0 : !fir.type<tp<{i:i32,d:f64}>>
+  }
+
+  gpu.module @cuda_device_mod {
+    gpu.func @_QMtestPkernel() kernel {
+      gpu.return
+    }
+    fir.global @_QMtestEtp_packed_dev {data_attr = #cuf.cuda<device>} : !fir.type<tp<{i:i32,d:f64}>> {
+      %0 = fir.zero_bits !fir.type<tp<{i:i32,d:f64}>>
+      fir.has_value %0 : !fir.type<tp<{i:i32,d:f64}>>
+    }
+  }
+}
+
+// Registered size must be 12 (packed: 4+8), not 16 (aligned unpacked size).
+// NOUNIFIED: arith.constant 12 : index
+// NOUNIFIED: fir.call @_FortranACUFRegisterVariable
+// UNIFIED: cuf.register_variable_static @_QMtestEtp_packed_dev("_QMtestEtp_packed_dev", 12) {deviceResident}
diff --git a/flang/test/Fir/CUDA/cuda-shared-offset.mlir b/flang/test/Fir/CUDA/cuda-shared-offset.mlir
index 68c31356a278b..8b5234f34619d 100644
--- a/flang/test/Fir/CUDA/cuda-shared-offset.mlir
+++ b/flang/test/Fir/CUDA/cuda-shared-offset.mlir
@@ -235,3 +235,47 @@ module attributes {dlti.dl_spec = #dlti.dl_spec<#dlti.dl_entry<!llvm.ptr, dense<
 // CHECK-LABEL: gpu.func @_QPt1()
 // CHECK: fir.global internal @_QPt1__shared_mem__s {alignment = 4 : i64, data_attr = #cuf.cuda<shared>} : !fir.array<1024xi8>
 // CHECK: fir.global external @_QPt1__shared_mem__ {alignment = 4 : i64, data_attr = #cuf.cuda<shared>} : !fir.array<0xi8>
+
+// -----
+
+// Tail-padded record in shared memory: !fir.type<tp{a:i32,b:i8}> has
+// 5 typed bytes but 8 allocation bytes (3 bytes of tail padding).
+// The shared memory global must be allocated as !fir.array<8xi8>, not
+// !fir.array<5xi8>, so subsequent shared variables start at the correct
+// aligned offset.
+
+module attributes {dlti.dl_spec = #dlti.dl_spec<#dlti.dl_entry<!llvm.ptr, dense<64> : vector<4xi64>>, #dlti.dl_entry<!llvm.ptr<271>, dense<32> : vector<4xi64>>, #dlti.dl_entry<!llvm.ptr<270>, dense<32> : vector<4xi64>>, #dlti.dl_entry<f128, dense<128> : vector<2xi64>>, #dlti.dl_entry<f64, dense<64> : vector<2xi64>>, #dlti.dl_entry<f80, dense<128> : vector<2xi64>>, #dlti.dl_entry<f16, dense<16> : vector<2xi64>>, #dlti.dl_entry<i32, dense<32> : vector<2xi64>>, #dlti.dl_entry<i16, dense<16> : vector<2xi64>>, #dlti.dl_entry<i128, dense<128> : vector<2xi64>>, #dlti.dl_entry<i8, dense<8> : vector<2xi64>>, #dlti.dl_entry<!llvm.ptr<272>, dense<64> : vector<4xi64>>, #dlti.dl_entry<i64, dense<64> : vector<2xi64>>, #dlti.dl_entry<i1, dense<8> : vector<2xi64>>, #dlti.dl_entry<"dlti.endianness", "little">, #dlti.dl_entry<"dlti.stack_alignment", 128 : i64>>, fir.defaultkind = "a1c4d8i4l4r4", fir.kindmap = "", gpu.container_module, llvm.data_layout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", llvm.target_triple = "x86_64-unknown-linux-gnu"} {
+  gpu.module @cuda_device_mod {
+    gpu.func @_QPshared_tail_padded() attributes {cuf.proc_attr = #cuf.cuda_proc<global>} {
+      %0 = cuf.shared_memory !fir.type<tp{a:i32,b:i8}> {bindc_name = "x", uniq_name = "_QFshared_tail_paddedEx"} -> !fir.ref<!fir.type<tp{a:i32,b:i8}>>
+      %1 = fir.declare %0 {data_attr = #cuf.cuda<shared>, uniq_name = "_QFshared_tail_paddedEx"} : (!fir.ref<!fir.type<tp{a:i32,b:i8}>>) -> !fir.ref<!fir.type<tp{a:i32,b:i8}>>
+      gpu.return
+    }
+  }
+}
+
+// Shared memory global must use the allocation size (8), not the typed
+// size (5).  Using 5 would misalign any variable placed after this one.
+// CHECK-LABEL: gpu.func @_QPshared_tail_padded()
+// CHECK: fir.global internal @_QPshared_tail_padded__shared_mem__x {alignment = 4 : i64, data_attr = #cuf.cuda<shared>} : !fir.array<8xi8>
+
+// -----
+
+// Packed record in shared memory: !fir.type<tp<{i:i32,d:f64}>> has no
+// alignment gaps and no tail padding; its size is 4+8=12 bytes (not 16).
+// The shared memory global must be !fir.array<12xi8>, not !fir.array<16xi8>.
+
+module attributes {dlti.dl_spec = #dlti.dl_spec<#dlti.dl_entry<!llvm.ptr, dense<64> : vector<4xi64>>, #dlti.dl_entry<!llvm.ptr<271>, dense<32> : vector<4xi64>>, #dlti.dl_entry<!llvm.ptr<270>, dense<32> : vector<4xi64>>, #dlti.dl_entry<f128, dense<128> : vector<2xi64>>, #dlti.dl_entry<f64, dense<64> : vector<2xi64>>, #dlti.dl_entry<f80, dense<128> : vector<2xi64>>, #dlti.dl_entry<f16, dense<16> : vector<2xi64>>, #dlti.dl_entry<i32, dense<32> : vector<2xi64>>, #dlti.dl_entry<i16, dense<16> : vector<2xi64>>, #dlti.dl_entry<i128, dense<128> : vector<2xi64>>, #dlti.dl_entry<i8, dense<8> : vector<2xi64>>, #dlti.dl_entry<!llvm.ptr<272>, dense<64> : vector<4xi64>>, #dlti.dl_entry<i64, dense<64> : vector<2xi64>>, #dlti.dl_entry<i1, dense<8> : vector<2xi64>>, #dlti.dl_entry<"dlti.endianness", "little">, #dlti.dl_entry<"dlti.stack_alignment", 128 : i64>>, fir.defaultkind = "a1c4d8i4l4r4", fir.kindmap = "", gpu.container_module, llvm.data_layout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", llvm.target_triple = "x86_64-unknown-linux-gnu"} {
+  gpu.module @cuda_device_mod {
+    gpu.func @_QPshared_packed() attributes {cuf.proc_attr = #cuf.cuda_proc<global>} {
+      %0 = cuf.shared_memory !fir.type<tp<{i:i32,d:f64}>> {bindc_name = "x", uniq_name = "_QFshared_packedEx"} -> !fir.ref<!fir.type<tp<{i:i32,d:f64}>>>
+      %1 = fir.declare %0 {data_attr = #cuf.cuda<shared>, uniq_name = "_QFshared_packedEx"} : (!fir.ref<!fir.type<tp<{i:i32,d:f64}>>>) -> !fir.ref<!fir.type<tp<{i:i32,d:f64}>>>
+      gpu.return
+    }
+  }
+}
+
+// Shared memory global must use the packed size (12), not the aligned
+// unpacked size (16).
+// CHECK-LABEL: gpu.func @_QPshared_packed()
+// CHECK: fir.global internal @_QPshared_packed__shared_mem__x {alignment = 1 : i64, data_attr = #cuf.cuda<shared>} : !fir.array<12xi8>
diff --git a/flang/test/Fir/box-elesize-canonicalize.fir b/flang/test/Fir/box-elesize-canonicalize.fir
index dd2b06ed3ef77..4a6a5532d27a5 100644
--- a/flang/test/Fir/box-elesize-canonicalize.fir
+++ b/flang/test/Fir/box-elesize-canonicalize.fir
@@ -102,4 +102,67 @@ module attributes { dlti.dl_spec = #dlti.dl_spec< i1 = dense<8> : vector<2xi64>,
   // CHECK:         %[[C:.*]] = arith.constant 16 : index
   // CHECK:         return %[[C]]
   // CHECK-NOT:     fir.box_elesize
+
+  // Fold: packed derived type -- fir.type<tp<{i:i32,d:f64}>>.
+  // This is the exact example from MattPD's review: on x86-64
+  // (f64 ABI align=8B) the non-packed size is 16B and the packed
+  // size is 12B (i32 allocSize=4B + f64 allocSize=alignTo(8,8)=8B,
+  // no inter-field gap, no tail padding).
+  // The element distance must be 12, not 16.
+  func.func @fold_packed_record(%arg0: !fir.box<!fir.type<tp<{i:i32,d:f64}>>>) -> index {
+    %0 = fir.box_elesize %arg0 : (!fir.box<!fir.type<tp<{i:i32,d:f64}>>>) -> index
+    return %0 : index
+  }
+  // CHECK-LABEL: func.func @fold_packed_record(
+  // CHECK:         %[[C:.*]] = arith.constant 12 : index
+  // CHECK:         return %[[C]]
+  // CHECK-NOT:     fir.box_elesize
+
+  // Fold: packed derived type with a wider alignment gap.
+  // On x86-64: i32 store=4B, complex<f64> store=16B.
+  // Non-packed: alignTo(4,4)=4 + alignTo(4,8)+alignTo(16,8)=24, tail=24 -> 24B.
+  // Packed:     4 + 16 = 20B, align=1.
+  // The element distance must be 20, not 24.
+  func.func @fold_packed_record2(%arg0: !fir.box<!fir.type<tp2<{i:i32,z:complex<f64>}>>>) -> index {
+    %0 = fir.box_elesize %arg0 : (!fir.box<!fir.type<tp2<{i:i32,z:complex<f64>}>>>) -> index
+    return %0 : index
+  }
+  // CHECK-LABEL: func.func @fold_packed_record2(
+  // CHECK:         %[[C:.*]] = arith.constant 20 : index
+  // CHECK:         return %[[C]]
+  // CHECK-NOT:     fir.box_elesize
+
+  // Fold: tail-padded record type -- fir.type<tpad{a:i32,b:i8}>.
+  // The field loop gives size=5, align=4; getTypeSizeAndAlignment returns
+  // {8, 4} (alignTo(5,4)=8).  FoldBoxEleSize also rounded 5 to 8 before
+  // this fix, so both revisions fold to 8.  This check verifies that the
+  // correct 8-byte element stride is produced; the distinction between
+  // allocation size and raw store size is covered by
+  // flang/test/Fir/CUDA/cuda-constructor-2.f90 and
+  // flang/test/Fir/CUDA/cuda-shared-offset.mlir.
+  // Use a distinct type name (tpad) to avoid redefining the packed type (tp)
+  // used above.
+  func.func @fold_tail_padded_record(%arg0: !fir.box<!fir.array<?x!fir.type<tpad{a:i32,b:i8}>>>) -> index {
+    %0 = fir.box_elesize %arg0 : (!fir.box<!fir.array<?x!fir.type<tpad{a:i32,b:i8}>>>) -> index
+    return %0 : index
+  }
+  // CHECK-LABEL: func.func @fold_tail_padded_record(
+  // CHECK:         %[[C:.*]] = arith.constant 8 : index
+  // CHECK:         return %[[C]]
+  // CHECK-NOT:     fir.box_elesize
+
+  // Fold: packed derived type with f80 component.
+  // f80 on x86-64: store size = 10B, ABI alignment = 16B,
+  //   allocation size = alignTo(10, 16) = 16B.
+  // i8:  store size = 1B, ABI alignment = 1B, allocation size = 1B.
+  // Packed size = 16 + 1 = 17B (not 10 + 1 = 11B).
+  // This case distinguishes allocation-size from store-size in the packed path.
+  func.func @fold_packed_f80_i8(%arg0: !fir.box<!fir.type<tp3<{x:f80,b:i8}>>>) -> index {
+    %0 = fir.box_elesize %arg0 : (!fir.box<!fir.type<tp3<{x:f80,b:i8}>>>) -> index
+    return %0 : index
+  }
+  // CHECK-LABEL: func.func @fold_packed_f80_i8(
+  // CHECK:         %[[C:.*]] = arith.constant 17 : index
+  // CHECK:         return %[[C]]
+  // CHECK-NOT:     fir.box_elesize
 }
diff --git a/flang/test/Fir/struct-passing-ppc64le-byval.fir b/flang/test/Fir/struct-passing-ppc64le-byval.fir
index b9be67fbe97bf..3717cb7d000d8 100644
--- a/flang/test/Fir/struct-passing-ppc64le-byval.fir
+++ b/flang/test/Fir/struct-passing-ppc64le-byval.fir
@@ -79,4 +79,12 @@ func.func @csub5(%arg0: !fir.type<_QFcsub5Tdt1{xdt0:!fir.type<_QFcsub5Tdt0{f1:co
 func.func @csub6(%arg0: !fir.type<_QFcsub6Tdt1{xdt0:!fir.type<_QFcsub6Tdt0{f1:complex<f32>}>,x1:f64}> {fir.bindc_name = "arg"}) attributes {fir.bindc_name = "csub6"} { return }
 //CHECK-LABEL: func.func @csub6(%arg0: !fir.array<2xi64> {fir.bindc_name = "arg"}) attributes {fir.bindc_name = "csub6"}
 
+// tail-padded struct: f128 (16B, align 16) + i8 (1B, align 1).
+// Store size = 17B; allocation size = alignTo(17, 16) = 32B.
+// nElem = ceil(32*8/64) = 4 -> [4 x i64].
+// clang --target=powerpc64le-unknown-linux-gnu confirms [2 x i128] = 4 doublewords.
+// The old unrounded size (17B) gave nElem=3 ([3 x i64]) -- a live C-interop ABI bug.
+func.func @csub_tailpad_f128_i8(%arg0: !fir.type<t{x:f128,c:i8}> {fir.bindc_name = "arg"}) attributes {fir.bindc_name = "csub_tailpad_f128_i8"} { return }
+//CHECK-LABEL: func.func @csub_tailpad_f128_i8(%arg0: !fir.array<4xi64> {fir.bindc_name = "arg"}) attributes {fir.bindc_name = "csub_tailpad_f128_i8"}
+
 }
diff --git a/flang/test/Lower/Intrinsics/transfer-rec-tail-pad.f90 b/flang/test/Lower/Intrinsics/transfer-rec-tail-pad.f90
new file mode 100644
index 0000000000000..ac7569778b64b
--- /dev/null
+++ b/flang/test/Lower/Intrinsics/transfer-rec-tail-pad.f90
@@ -0,0 +1,74 @@
+! RUN: %flang_fc1 -triple x86_64-unknown-linux-gnu -emit-hlfir %s -o - | FileCheck %s
+! REQUIRES: x86-registered-target
+
+! Regression test for https://github.com/llvm/llvm-project/pull/220377
+!
+! The TRANSFER inline gate compares store sizes, not allocation sizes.
+! Tail padding must not be included in the comparison.
+!
+! Types declared at module scope as a workaround for a pre-existing
+! FIR-to-LLVM lowering failure: bare %flang_fc1 aborts with a missing
+! type-descriptor error when lowering boxes for derived types.  The abort
+! occurs in FIR-to-LLVM lowering regardless of declaration scope; it can
+! be suppressed with -mmlir --ignore-missing-type-desc (a debug flag) but
+! that is not appropriate for a regression test.  Placing the types in a
+! module avoids boxing them at the HLFIR stage exercised here.
+
+! Shape 1: tail-padded record whose store size matches real(10).
+!   t1 fields: integer(8) [8B, align 8] + integer(2) [2B, align 2]
+!   Store size = 10B; allocation size = alignTo(10, 8) = 16B.
+!   real(10) = f80 on x86-64: store size = 10B.
+!   10 == 10 on store size -> must INLINE (fir.load), not call _FortranATransfer.
+!   Without the storeSizeOnly fix the allocation size (16) != 10 and this
+!   would wrongly fall through to the runtime path.
+module m1
+  type :: t1
+    integer(8) :: a
+    integer(2) :: b
+  end type
+end module
+
+! Shape 2: tail-padded record whose allocation size matches integer(8) but
+!   whose store size does not.
+!   t2 fields: integer(4) [4B, align 4] + integer(1) [1B, align 1]
+!   Store size = 5B; allocation size = alignTo(5, 4) = 8B.
+!   integer(8) store size = 8B.
+!   Without the storeSizeOnly fix: 8 == 8 on allocation size -> would inline,
+!   but that reads 3 bytes of uninitialized tail padding into the result.
+!   With the fix: 5 != 8 on store size -> correctly stays on RUNTIME path.
+module m2
+  type :: t2
+    integer(4) :: a
+    integer(1) :: b
+  end type
+end module
+
+subroutine transfer_rec_to_real10(out)
+  ! CHECK-LABEL: func @_QPtransfer_rec_to_real10(
+  ! CHECK-NOT:     fir.call @_FortranATransfer
+  ! CHECK:         %[[TMP:.*]] = fir.alloca f80
+  ! CHECK:         %[[REC:.*]] = fir.load {{.*}} : !fir.ref<!fir.type<{{.*}}>>
+  ! CHECK:         %[[TMPSRC:.*]] = fir.convert %[[TMP]] : (!fir.ref<f80>) -> !fir.ref<!fir.type<{{.*}}>>
+  ! CHECK:         fir.store %[[REC]] to %[[TMPSRC]] : !fir.ref<!fir.type<{{.*}}>>
+  ! CHECK:         fir.load %[[TMP]] : !fir.ref<f80>
+  ! CHECK:         return
+  use m1
+  type(t1) :: src
+  real(10) :: out
+  src%a = 42
+  src%b = 7
+  out = transfer(src, out)
+end subroutine
+
+subroutine transfer_rec_to_int8(res)
+  ! CHECK-LABEL: func @_QPtransfer_rec_to_int8(
+  ! CHECK:         fir.call @_FortranATransfer
+  ! CHECK-NOT:     fir.load {{.*}} : !fir.ref<i64>
+  ! CHECK:         return
+  use m2
+  type(t2) :: x
+  integer(8) :: res
+  x%a = 1
+  x%b = 2_1
+  res = transfer(x, res)
+end subroutine
diff --git a/flang/test/Transforms/allocation-placement.fir b/flang/test/Transforms/allocation-placement.fir
index 8b0a1acb74122..bccab6f713f21 100644
--- a/flang/test/Transforms/allocation-placement.fir
+++ b/flang/test/Transforms/allocation-placement.fir
@@ -102,4 +102,48 @@ func.func @small_temp_in_acc_parallel() {
   return
 }
 
+// Tail-padded record array: !fir.array<3x!fir.type<tpad{a:i32,b:i8}>>
+// Each element is 8 bytes (5 store + 3 tail padding), total = 24 bytes.
+// SequenceType rounds each element to its alignment boundary (alignTo(5,4)=8)
+// before multiplying by the element count, so both the pre-fix and post-fix
+// code produce 24 bytes.  This case verifies that per-element SequenceType
+// rounding is correct, not that the tail-padding fix changes the total.
+// With the default 64-byte threshold the 24-byte array is promoted to stack.
+// CHECK-LABEL: func.func @tail_padded_record_placement
+// CHECK: fir.alloca !fir.array<3x!fir.type<tpad{a:i32,b:i8}>>
+// CHECK-NOT: fir.allocmem
+func.func @tail_padded_record_placement() {
+  %0 = fir.allocmem !fir.array<3x!fir.type<tpad{a:i32,b:i8}>>
+  %c0 = arith.constant 0 : index
+  %v = arith.constant 0 : i32
+  %r = fir.convert %0 : (!fir.heap<!fir.array<3x!fir.type<tpad{a:i32,b:i8}>>>) -> !fir.ref<!fir.array<3x!fir.type<tpad{a:i32,b:i8}>>>
+  %e = fir.coordinate_of %r, %c0 : (!fir.ref<!fir.array<3x!fir.type<tpad{a:i32,b:i8}>>>, index) -> !fir.ref<!fir.type<tpad{a:i32,b:i8}>>
+  %f = fir.coordinate_of %e, %c0 : (!fir.ref<!fir.type<tpad{a:i32,b:i8}>>, index) -> !fir.ref<i32>
+  fir.store %v to %f : !fir.ref<i32>
+  fir.freemem %0 : !fir.heap<!fir.array<3x!fir.type<tpad{a:i32,b:i8}>>>
+  return
+}
+
+// Packed record array: !fir.array<5x!fir.type<tpk<{i:i32,d:f64}>>>
+// Each element is 12 bytes (packed: 4+8, no gaps or tail padding), total=60B.
+// Without the isPacked fix, getTypeSizeAndAlignment returns 16 per element
+// (aligned like an unpacked struct), giving a wrong total of 80 bytes which
+// exceeds the 64-byte threshold and would select heap placement.  With the
+// fix the correct total of 60 bytes is below the threshold and the array
+// must be promoted to stack.
+// CHECK-LABEL: func.func @packed_record_placement
+// CHECK: fir.alloca !fir.array<5x!fir.type<tpk<{i:i32,d:f64}>>>
+// CHECK-NOT: fir.allocmem
+func.func @packed_record_placement() {
+  %0 = fir.allocmem !fir.array<5x!fir.type<tpk<{i:i32,d:f64}>>>
+  %c0 = arith.constant 0 : index
+  %v = arith.constant 0 : i32
+  %r = fir.convert %0 : (!fir.heap<!fir.array<5x!fir.type<tpk<{i:i32,d:f64}>>>>) -> !fir.ref<!fir.array<5x!fir.type<tpk<{i:i32,d:f64}>>>>
+  %e = fir.coordinate_of %r, %c0 : (!fir.ref<!fir.array<5x!fir.type<tpk<{i:i32,d:f64}>>>>, index) -> !fir.ref<!fir.type<tpk<{i:i32,d:f64}>>>
+  %f = fir.coordinate_of %e, %c0 : (!fir.ref<!fir.type<tpk<{i:i32,d:f64}>>>, index) -> !fir.ref<i32>
+  fir.store %v to %f : !fir.ref<i32>
+  fir.freemem %0 : !fir.heap<!fir.array<5x!fir.type<tpk<{i:i32,d:f64}>>>>
+  return
+}
+
 }

>From 3be2c99e853fdfc1b32951796ae121886f143a9d Mon Sep 17 00:00:00 2001
From: Daniel Chen <cdchen at ca.ibm.com>
Date: Thu, 3 Sep 2026 21:02:24 -0400
Subject: [PATCH 2/2] [flang] Preserve TRANSFER padding bytes

---
 flang/lib/Optimizer/Builder/IntrinsicCall.cpp | 13 ++++---
 .../Fir/struct-passing-systemz-reference.fir  | 34 +++++++++++++++++++
 .../Intrinsics/transfer-rec-tail-pad-llvm.f90 | 28 +++++++++++++++
 .../Intrinsics/transfer-rec-tail-pad.f90      | 26 ++++++++++++++
 4 files changed, 97 insertions(+), 4 deletions(-)
 create mode 100644 flang/test/Lower/Intrinsics/transfer-rec-tail-pad-llvm.f90

diff --git a/flang/lib/Optimizer/Builder/IntrinsicCall.cpp b/flang/lib/Optimizer/Builder/IntrinsicCall.cpp
index 6a71a3d9a14b9..47ad593af29bc 100644
--- a/flang/lib/Optimizer/Builder/IntrinsicCall.cpp
+++ b/flang/lib/Optimizer/Builder/IntrinsicCall.cpp
@@ -9025,10 +9025,15 @@ IntrinsicLibrary::genTransfer(mlir::Type resultType,
           return fir::LoadOp::create(builder, loc, cast);
         }
         mlir::Value tmp = fir::AllocaOp::create(builder, loc, resultType);
-        mlir::Value srcLoad = fir::LoadOp::create(builder, loc, sourceBase);
-        mlir::Type srcRefTy = builder.getRefType(sourceType);
-        mlir::Value tmpAsSrc = builder.createConvert(loc, srcRefTy, tmp);
-        fir::StoreOp::create(builder, loc, srcLoad, tmpAsSrc);
+        mlir::Type byteType = fir::SequenceType::get(
+            {static_cast<int64_t>(sourceSizeAndAlign->first)},
+            builder.getI8Type());
+        mlir::Type byteRefType = builder.getRefType(byteType);
+        mlir::Value sourceBytes = builder.createConvert(loc, byteRefType,
+                                                         sourceBase);
+        mlir::Value resultBytes = builder.createConvert(loc, byteRefType, tmp);
+        fir::CopyOp::create(builder, loc, sourceBytes, resultBytes,
+                            /*noOverlap=*/true);
         return fir::LoadOp::create(builder, loc, tmp);
       }
     }
diff --git a/flang/test/Fir/struct-passing-systemz-reference.fir b/flang/test/Fir/struct-passing-systemz-reference.fir
index 353bd8c47292f..1e425ef6e66fd 100644
--- a/flang/test/Fir/struct-passing-systemz-reference.fir
+++ b/flang/test/Fir/struct-passing-systemz-reference.fir
@@ -102,3 +102,37 @@ func.func @test_call_large(%arg0: !fir.ref<!fir.type<t5{i:!fir.array<8xi32>}>>)
   return
 }
 
+// BIND(C) records with tail padding are passed in the corresponding GPR size.
+// CHECK-LABEL: func.func private @test_i32_i8(%arg0: i64)
+func.func private @test_i32_i8(%arg0: !fir.type<t11{i:i32,c:i8}>) { return }
+
+// CHECK-LABEL: func.func private @test_i16_i8(%arg0: i32)
+func.func private @test_i16_i8(%arg0: !fir.type<t12{i:i16,c:i8}>) { return }
+
+// CHECK-LABEL: func.func @test_call_i32_i8(
+// CHECK-SAME:    %[[ARG0:.*]]: !fir.ref<!fir.type<t11{i:i32,c:i8}>>) {
+func.func @test_call_i32_i8(%arg0: !fir.ref<!fir.type<t11{i:i32,c:i8}>>) {
+  // CHECK: %[[IN:.*]] = fir.load %[[ARG0]] : !fir.ref<!fir.type<t11{i:i32,c:i8}>>
+  %in = fir.load %arg0 : !fir.ref<!fir.type<t11{i:i32,c:i8}>>
+  // CHECK: %[[TMP:.*]] = fir.alloca i64
+  // CHECK: %[[CVT:.*]] = fir.convert %[[TMP]] : (!fir.ref<i64>) -> !fir.ref<!fir.type<t11{i:i32,c:i8}>>
+  // CHECK: fir.store %[[IN]] to %[[CVT]] : !fir.ref<!fir.type<t11{i:i32,c:i8}>>
+  // CHECK: %[[VAL:.*]] = fir.load %[[TMP]] : !fir.ref<i64>
+  // CHECK: fir.call @test_i32_i8(%[[VAL]]) : (i64) -> ()
+  fir.call @test_i32_i8(%in) : (!fir.type<t11{i:i32,c:i8}>) -> ()
+  return
+}
+
+// CHECK-LABEL: func.func @test_call_i16_i8(
+// CHECK-SAME:    %[[ARG0:.*]]: !fir.ref<!fir.type<t12{i:i16,c:i8}>>) {
+func.func @test_call_i16_i8(%arg0: !fir.ref<!fir.type<t12{i:i16,c:i8}>>) {
+  // CHECK: %[[IN:.*]] = fir.load %[[ARG0]] : !fir.ref<!fir.type<t12{i:i16,c:i8}>>
+  %in = fir.load %arg0 : !fir.ref<!fir.type<t12{i:i16,c:i8}>>
+  // CHECK: %[[TMP:.*]] = fir.alloca i32
+  // CHECK: %[[CVT:.*]] = fir.convert %[[TMP]] : (!fir.ref<i32>) -> !fir.ref<!fir.type<t12{i:i16,c:i8}>>
+  // CHECK: fir.store %[[IN]] to %[[CVT]] : !fir.ref<!fir.type<t12{i:i16,c:i8}>>
+  // CHECK: %[[VAL:.*]] = fir.load %[[TMP]] : !fir.ref<i32>
+  // CHECK: fir.call @test_i16_i8(%[[VAL]]) : (i32) -> ()
+  fir.call @test_i16_i8(%in) : (!fir.type<t12{i:i16,c:i8}>) -> ()
+  return
+}
diff --git a/flang/test/Lower/Intrinsics/transfer-rec-tail-pad-llvm.f90 b/flang/test/Lower/Intrinsics/transfer-rec-tail-pad-llvm.f90
new file mode 100644
index 0000000000000..273acd4b9cb53
--- /dev/null
+++ b/flang/test/Lower/Intrinsics/transfer-rec-tail-pad-llvm.f90
@@ -0,0 +1,28 @@
+! RUN: %flang_fc1 -triple x86_64-unknown-linux-gnu -emit-llvm %s -o - | FileCheck %s
+! REQUIRES: x86-registered-target
+
+! Regression test for https://github.com/llvm/llvm-project/pull/220377
+!
+! The source record has a 10-byte stored representation and 16-byte allocation
+! extent.  TRANSFER to real(10) must copy the 10 bytes into result-aligned
+! storage before loading the result.
+module m
+  type :: t
+    integer(8) :: a
+    integer(2) :: b
+  end type
+end module
+
+subroutine transfer_rec_to_real10(out)
+  use m
+  type(t) :: src
+  real(10) :: out
+  src%a = 42
+  src%b = 7
+  out = transfer(src, out)
+end subroutine
+
+! CHECK-LABEL: define{{.*}} @_QPtransfer_rec_to_real10(
+! CHECK:       %[[TMP:.*]] = alloca x86_fp80
+! CHECK:       call void @llvm.memcpy.p0.p0.i64(ptr align 16 %[[TMP]], ptr align 8 {{.*}}, i64 10, i1 false)
+! CHECK:       %[[RESULT:.*]] = load x86_fp80, ptr %[[TMP]], align 16
diff --git a/flang/test/Lower/Intrinsics/transfer-rec-tail-pad.f90 b/flang/test/Lower/Intrinsics/transfer-rec-tail-pad.f90
index ac7569778b64b..bf061e2f8225e 100644
--- a/flang/test/Lower/Intrinsics/transfer-rec-tail-pad.f90
+++ b/flang/test/Lower/Intrinsics/transfer-rec-tail-pad.f90
@@ -72,3 +72,29 @@ subroutine transfer_rec_to_int8(res)
   x%b = 2_1
   res = transfer(x, res)
 end subroutine
+! A BIND(C) record may contain internal padding.  TRANSFER must copy the
+! physical bytes rather than loading and storing the record aggregate, since
+! the latter can replace padding bytes with undef.
+subroutine transfer_bindc_record_to_int128(res)
+  ! CHECK-LABEL: func @_QPtransfer_bindc_record_to_int128(
+  ! CHECK:         fir.alloca i128
+  ! CHECK:         fir.convert {{.*}} -> !fir.ref<!fir.array<16xi8>>
+  ! CHECK:         fir.convert {{.*}} -> !fir.ref<!fir.array<16xi8>>
+  ! CHECK:         fir.copy {{.*}} : (!fir.ref<!fir.array<16xi8>>, !fir.ref<!fir.array<16xi8>>)
+  ! CHECK:         fir.load {{.*}} : !fir.ref<i128>
+  use iso_c_binding, only: c_int8_t, c_int64_t
+  type, bind(c) :: t
+    integer(c_int8_t) :: first
+    integer(c_int64_t) :: rest
+  end type
+  type(t) :: source
+  integer(16) :: res
+  integer(c_int8_t) :: bytes(16) = [ &
+      1_c_int8_t, 2_c_int8_t, 3_c_int8_t, 4_c_int8_t, &
+      5_c_int8_t, 6_c_int8_t, 7_c_int8_t, 8_c_int8_t, &
+      9_c_int8_t, 10_c_int8_t, 11_c_int8_t, 12_c_int8_t, &
+      13_c_int8_t, 14_c_int8_t, 15_c_int8_t, 16_c_int8_t]
+  ! Initialize all 16 bytes, including the seven internal padding bytes.
+  source = transfer(bytes, source)
+  res = transfer(source, res)
+end subroutine



More information about the flang-commits mailing list