[flang-commits] [flang] [flang] Fix RecordType sizes, TRANSFER lowering, and BIND(C) ABI on SystemZ/PPC64le (PR #220377)

Daniel Chen via flang-commits flang-commits at lists.llvm.org
Fri Sep 4 20:38:54 PDT 2026


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

>From de5ef3bc48d664a87239e05c6168fd3f94f302c0 Mon Sep 17 00:00:00 2001
From: Daniel Chen <cdchen at ca.ibm.com>
Date: Fri, 4 Sep 2026 11:03:01 -0400
Subject: [PATCH] [flang] Fix RecordType sizes, TRANSFER lowering, and BIND(C)
 ABI on SystemZ/PPC64le

fir::getTypeSizeAndAlignment had two bugs in its RecordType branch:
- Packed records: isPacked() was ignored, giving wrong sizes (e.g. packed
  {f80,i8} on x86-64 returned 11 bytes instead of 17).
- Tail padding: the field loop omitted the final alignTo(size, align),
  so {i32,i8} returned 5 bytes instead of 8.

Add a getTypeStoreSizeAndAlignment API that returns the stored-representation
width excluding only the outermost tail padding, distinct from both the
allocation extent and the Fortran STORAGE_SIZE result.

Fix the TRANSFER inline gate (genTransfer) to compare stored-representation
widths instead of allocation sizes, avoiding both wrong inlining (reading
tail-padding bytes) and missed inlining. For RecordType sources where
sourceAlign < resultAlign, copy via fir.copy into a result-aligned alloca
instead of a direct load, fixing alignment UB and preserving inter-field
padding bytes per F2023 16.9.212.

Fix BIND(C) / VALUE argument passing on SystemZ and PPC64le: the corrected
allocation sizes now match the C ABI (e.g. {i32,i8} passes as i64 on
SystemZ, {f128,i8} as [4xi64] on PPC64le). Programs using BIND(C) VALUE
arguments of these shapes were already ABI-incompatible with C; all-Fortran
programs relying on the old convention must be recompiled.

Tests added:
- transfer-rec-tail-pad.f90: HLFIR checks for inline/runtime path selection
  and BIND(C) internal-padding preservation via fir.copy.
- transfer-rec-tail-pad-llvm.f90: LLVM IR check for the alloca + memcpy +
  aligned-load sequence when transferring a tail-padded record to real(10).
- box-elesize-canonicalize.fir: fir.box_elesize fold cases for packed and
  tail-padded record element strides.
- allocation-placement.fir: stack/heap placement threshold cases for packed
  and tail-padded record arrays.
- cuda-constructor-2.f90: correct allocation sizes registered for packed and
  tail-padded CUDA device globals.
- cuda-shared-offset.mlir: correct allocation sizes for packed and
  tail-padded CUDA shared memory variables.
- struct-passing-systemz-reference.fir: {i32,i8} passed as i64 and {i16,i8}
  passed as i32 on SystemZ, with call-site lowering checks.
- struct-passing-ppc64le-byval.fir: {f128,i8} passed as [4xi64] on PPC64le.
---
 flang/docs/ReleaseNotes.md                    |  42 ++++++++
 .../include/flang/Optimizer/Dialect/FIRType.h |  41 ++++++-
 flang/lib/Optimizer/Builder/IntrinsicCall.cpp |  76 +++++++++++--
 flang/lib/Optimizer/Dialect/FIROps.cpp        |   8 +-
 flang/lib/Optimizer/Dialect/FIRType.cpp       |  69 ++++++++++--
 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 ++
 .../Fir/struct-passing-systemz-reference.fir  |  34 ++++++
 .../Intrinsics/transfer-rec-tail-pad-llvm.f90 |  28 +++++
 .../Intrinsics/transfer-rec-tail-pad.f90      | 100 ++++++++++++++++++
 .../test/Transforms/allocation-placement.fir  |  44 ++++++++
 13 files changed, 596 insertions(+), 23 deletions(-)
 create mode 100644 flang/test/Lower/Intrinsics/transfer-rec-tail-pad-llvm.f90
 create mode 100644 flang/test/Lower/Intrinsics/transfer-rec-tail-pad.f90

diff --git a/flang/docs/ReleaseNotes.md b/flang/docs/ReleaseNotes.md
index 0f5c1548f943d..78be5f082b0a5 100644
--- a/flang/docs/ReleaseNotes.md
+++ b/flang/docs/ReleaseNotes.md
@@ -31,6 +31,48 @@ page](https://llvm.org/releases/).
 
 ## Bug Fixes
 
+- Fixed `fir::getTypeSizeAndAlignment` returning the wrong allocation size for
+  **packed derived types** (`SEQUENCE` types compiled with `PACK` / LLVM
+  packed-struct layout). Fields in a packed struct are placed back-to-back
+  using each component's allocation size (`alignTo(storeSize, ABIalign)`), not
+  its raw store size, and the struct ABI alignment is 1. For example, a packed
+  `{f80, i8}` on x86-64 now correctly reports 17 bytes instead of 11.
+  ([#220377](https://github.com/llvm/llvm-project/pull/220377))
+
+- Fixed `fir::getTypeSizeAndAlignment` omitting **tail padding** from unpacked
+  derived types. The returned size is now rounded up to the record's own ABI
+  alignment, matching the allocation extent used by array element strides, CUDA
+  shared-memory layout, and stack/heap allocation placement. For example,
+  `{i32, i8}` (store size 5 bytes, align 4) now correctly reports 8 bytes
+  instead of 5.
+  ([#220377](https://github.com/llvm/llvm-project/pull/220377))
+
+- Fixed a **`BIND(C)` / `VALUE` argument-passing ABI bug** on SystemZ and
+  PPC64le: derived types whose allocation size fits in a GPR were incorrectly
+  passed indirectly (by reference) instead of as an integer register value,
+  because `getTypeSizeAndAlignment` was returning the unpadded store size
+  rather than the allocation size. For example, `{i32, i8}` (allocation size
+  8 bytes) is now correctly passed as `i64` on SystemZ, and `{f128, i8}`
+  (allocation size 32 bytes) as `[4 x i64]` on PPC64le, matching the C ABI.
+  Fortran programs with `BIND(C)` `VALUE` derived-type arguments of these
+  shapes that interoperate with C were already producing incorrect results;
+  programs compiled entirely in Fortran that relied on the old (incorrect)
+  convention must be recompiled.
+  ([#220377](https://github.com/llvm/llvm-project/pull/220377))
+
+- Fixed the `TRANSFER` intrinsic inline path to compare **stored-representation
+  widths** (excluding outer tail padding) rather than allocation sizes when
+  deciding whether to inline a load instead of calling the runtime. This
+  prevents reading uninitialized tail-padding bytes into the result (wrong
+  inlining) and avoids incorrectly falling back to the runtime for records
+  whose store size matches the mold (missed inlining). The inline path now
+  also byte-copies record data into result-aligned storage so that internal
+  padding bytes (e.g. in `BIND(C)` records) are preserved, satisfying the
+  F2023 16.9.212 requirement that the result's physical representation be
+  identical to the source's when both have the same length.
+  ([#220377](https://github.com/llvm/llvm-project/pull/220377))
+
+
 ## Non-comprehensive list of changes in this release
 
 - Added support for the OpenMP implementation-defined extension sentinels
diff --git a/flang/include/flang/Optimizer/Dialect/FIRType.h b/flang/include/flang/Optimizer/Dialect/FIRType.h
index 6684af86a33fa..e285464c1a754 100644
--- a/flang/include/flang/Optimizer/Dialect/FIRType.h
+++ b/flang/include/flang/Optimizer/Dialect/FIRType.h
@@ -555,13 +555,52 @@ inline bool isRefOfConstantSizeAggregateType(mlir::Type t) {
 std::string getTypeAsString(mlir::Type ty, const KindMapping &kindMap,
                             llvm::StringRef prefix = "");
 
-/// Return the size and alignment of FIR types.
+/// Return the allocation extent and ABI alignment of a FIR type.
+///
+/// The returned size includes padding required for allocation and array
+/// element strides. For unpacked RecordType it may differ from the
+/// stored-representation width returned by getTypeStoreSizeAndAlignment
+/// and from the Fortran STORAGE_SIZE result.
+///
+/// - **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).
+///
+/// - **Sequences (fir::SequenceType)**:
+///   Each element size is rounded to its alignment boundary before
+///   multiplying by the element count (allocation-size stride). This
+///   rounding is part of the allocation extent and 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.
+///
+/// - **Unpacked records (fir::RecordType, not packed)**:
+///   Fields are laid out with inter-field alignment padding; each field
+///   occupies llvm::alignTo(fieldSize, fieldAlign) bytes. The allocation
+///   extent is rounded up to the record's own alignment (tail-padded).
+///
 /// 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
 /// straightforward given the need for a kind map that would need to be
 /// converted in terms of mlir::DataLayoutEntryKey.
 
+/// Return the stored-representation width and ABI alignment of a FIR type.
+/// The width excludes trailing padding from the outermost object, but
+/// preserves padding within the representation. It can differ from the
+/// Fortran STORAGE_SIZE intrinsic result and is distinct from descriptor
+/// element extent and runtime TRANSFER copy width.
+std::optional<std::pair<uint64_t, unsigned short>>
+getTypeStoreSizeAndAlignment(mlir::Location loc, mlir::Type ty,
+                             const mlir::DataLayout &dl,
+                             const fir::KindMapping &kindMap);
+
 /// This variant terminates the compilation if an unsupported type is passed.
 std::pair<std::uint64_t, unsigned short>
 getTypeSizeAndAlignmentOrCrash(mlir::Location loc, mlir::Type ty,
diff --git a/flang/lib/Optimizer/Builder/IntrinsicCall.cpp b/flang/lib/Optimizer/Builder/IntrinsicCall.cpp
index a139af9a29e57..65336fed06fd3 100644
--- a/flang/lib/Optimizer/Builder/IntrinsicCall.cpp
+++ b/flang/lib/Optimizer/Builder/IntrinsicCall.cpp
@@ -8972,22 +8972,76 @@ IntrinsicLibrary::genTransfer(mlir::Type resultType,
         (fir::isa_trivial(sourceType) ||
          mlir::isa<fir::RecordType>(sourceType)) &&
         fir::isa_trivial(moldType)) {
-      auto sourceSizeAndAlign = fir::getTypeSizeAndAlignment(
+      // Compare stored-representation widths, excluding outer tail padding
+      // from an unpacked RecordType.  This is distinct from STORAGE_SIZE,
+      // descriptor element extent, and the runtime TRANSFER copy width.
+      // 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::getTypeStoreSizeAndAlignment(
           loc, sourceType, builder.getDataLayout(), builder.getKindMap());
-      auto resultSizeAndAlign = fir::getTypeSizeAndAlignment(
+      auto resultSizeAndAlign = fir::getTypeStoreSizeAndAlignment(
           loc, resultType, builder.getDataLayout(), builder.getKindMap());
       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 sourceAlign >= resultAlign, a direct address cast and load is
+        // safe: the existing source storage satisfies the result type's
+        // alignment requirement.
+        //
+        // When sourceAlign < resultAlign (e.g. {i64,i16} is 8-byte aligned
+        // while x86_fp80 requires 16-byte alignment), loading resultType
+        // directly from sourceBase would assert an over-aligned address and
+        // produce undefined behaviour.  In that case, copy the store-size
+        // bytes into a result-typed alloca (which has resultType's natural
+        // alignment) using fir.copy (a non-overlapping byte copy, equivalent
+        // to memcpy), then load from the properly-aligned alloca.
+        //
+        // Note: fir.copy copies exactly sourceSizeAndAlign->first bytes (the
+        // store size, excluding outermost tail padding).  Inter-field padding
+        // bytes within the record are included in the store size and are
+        // therefore preserved, satisfying F2023 16.9.212.
+        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::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/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..79b8d43714e08 100644
--- a/flang/lib/Optimizer/Dialect/FIRType.cpp
+++ b/flang/lib/Optimizer/Dialect/FIRType.cpp
@@ -1638,10 +1638,11 @@ 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) {
+static std::optional<std::pair<uint64_t, unsigned short>>
+getTypeSizeAndAlignmentImpl(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 +1651,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 = getTypeSizeAndAlignmentImpl(loc, seqTy.getEleTy(), dl,
+                                              kindMap, storeSizeOnly);
     if (!result)
       return result;
     auto [eleSize, eleAlign] = *result;
@@ -1661,8 +1663,37 @@ 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()) {
+        // Use the component's allocation size (storeSizeOnly=false) regardless
+        // of the outer storeSizeOnly flag.  LLVM's packed StructLayout advances
+        // by getTypeAllocSize per field, so we need the allocation size of each
+        // component to reproduce the correct packed layout.  The outer
+        // storeSizeOnly flag is irrelevant here because packed structs have no
+        // tail padding; the struct's total size is the same whether or not tail
+        // rounding is requested.
+        auto result =
+            getTypeSizeAndAlignmentImpl(loc, component.second, dl, kindMap,
+                                        /*storeSizeOnly=*/false);
+        if (!result)
+          return result;
+        auto [compSize, compAlign] = *result;
+        size += llvm::alignTo(compSize, compAlign); // alloc 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 = getTypeSizeAndAlignmentImpl(loc, component.second, dl,
+                                                kindMap, storeSizeOnly);
       if (!result)
         return result;
       auto [compSize, compAlign] = *result;
@@ -1670,18 +1701,24 @@ fir::getTypeSizeAndAlignment(mlir::Location loc, mlir::Type ty,
           llvm::alignTo(size, compAlign) + llvm::alignTo(compSize, compAlign);
       align = std::max(align, compAlign);
     }
+    // Round up to the record's alignment to include outermost tail padding.
+    // storeSizeOnly suppresses only this final rounding; inter-field padding
+    // within the record is always included.
+    if (!storeSizeOnly)
+      size = llvm::alignTo(size, align);
     return std::pair{size, align};
   }
   if (auto logical = mlir::dyn_cast<fir::LogicalType>(ty)) {
     mlir::Type intTy = mlir::IntegerType::get(
         logical.getContext(), kindMap.getLogicalBitsize(logical.getFKind()));
-    return getTypeSizeAndAlignment(loc, intTy, dl, kindMap);
+    return getTypeSizeAndAlignmentImpl(loc, intTy, dl, kindMap, storeSizeOnly);
   }
   if (auto character = mlir::dyn_cast<fir::CharacterType>(ty)) {
     mlir::Type intTy = mlir::IntegerType::get(
         character.getContext(),
         kindMap.getCharacterBitsize(character.getFKind()));
-    auto result = getTypeSizeAndAlignment(loc, intTy, dl, kindMap);
+    auto result =
+        getTypeSizeAndAlignmentImpl(loc, intTy, dl, kindMap, storeSizeOnly);
     if (!result)
       return result;
     auto [compSize, compAlign] = *result;
@@ -1692,6 +1729,22 @@ fir::getTypeSizeAndAlignment(mlir::Location loc, mlir::Type ty,
   return std::nullopt;
 }
 
+std::optional<std::pair<uint64_t, unsigned short>>
+fir::getTypeSizeAndAlignment(mlir::Location loc, mlir::Type ty,
+                             const mlir::DataLayout &dl,
+                             const fir::KindMapping &kindMap) {
+  return getTypeSizeAndAlignmentImpl(loc, ty, dl, kindMap,
+                                     /*storeSizeOnly=*/false);
+}
+
+std::optional<std::pair<uint64_t, unsigned short>>
+fir::getTypeStoreSizeAndAlignment(mlir::Location loc, mlir::Type ty,
+                                  const mlir::DataLayout &dl,
+                                  const fir::KindMapping &kindMap) {
+  return getTypeSizeAndAlignmentImpl(loc, ty, dl, kindMap,
+                                     /*storeSizeOnly=*/true);
+}
+
 std::pair<std::uint64_t, unsigned short>
 fir::getTypeSizeAndAlignmentOrCrash(mlir::Location loc, mlir::Type ty,
                                     const mlir::DataLayout &dl,
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/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..410b6624519b3
--- /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{{.*}} @transfer_rec_to_real10_(
+! CHECK:       %[[TMP:.*]] = alloca x86_fp80
+! CHECK:       call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, 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
new file mode 100644
index 0000000000000..eabbbe1ed22ae
--- /dev/null
+++ b/flang/test/Lower/Intrinsics/transfer-rec-tail-pad.f90
@@ -0,0 +1,100 @@
+! 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.
+!
+! The full file lowers to LLVM IR with the intrinsic module path used by lit.
+! The module-scoped t2 routine still emits fir.embox; the LLVM lowering path
+! handles its descriptor in this test.
+
+! 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:         fir.convert {{.*}} : (!fir.ref<!fir.type<{{.*}}>>) -> !fir.ref<!fir.array<10xi8>>
+  ! CHECK:         fir.convert {{.*}} : (!fir.ref<f80>) -> !fir.ref<!fir.array<10xi8>>
+  ! CHECK:         fir.copy {{.*}} to {{.*}} no_overlap : !fir.ref<!fir.array<10xi8>>, !fir.ref<!fir.array<10xi8>>
+  ! CHECK:         fir.load {{.*}} : !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
+! 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:         %[[TMP128:.*]] = fir.alloca i128
+  ! CHECK:         fir.convert {{.*}} : (!fir.ref<!fir.type<{{.*}}>>) -> !fir.ref<!fir.array<16xi8>>
+  ! CHECK:         fir.convert {{.*}} : (!fir.ref<i128>) -> !fir.ref<!fir.array<16xi8>>
+  ! CHECK:         fir.copy {{.*}} to {{.*}} no_overlap : !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, c_loc, c_f_pointer
+  type, bind(c) :: t
+    integer(c_int8_t) :: first
+    integer(c_int64_t) :: rest
+  end type
+  type(t), target :: source
+  integer(16), target :: res
+  integer(c_int8_t), target :: 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]
+  integer(c_int8_t), pointer :: source_bytes(:), result_bytes(:)
+  ! Initialize all 16 bytes, including the seven internal padding bytes.
+  call c_f_pointer(c_loc(source), source_bytes, [16])
+  source_bytes = bytes
+  res = transfer(source, res)
+  call c_f_pointer(c_loc(res), result_bytes, [16])
+  if (any(result_bytes /= bytes)) error stop 'TRANSFER changed bytes'
+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
+}
+
 }



More information about the flang-commits mailing list