[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
Wed Sep 16 03:33:08 PDT 2026


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

>From e43eefd4ce78be95726249e16f3f72016557031a Mon Sep 17 00:00:00 2001
From: Daniel Chen <cdchen at ca.ibm.com>
Date: Mon, 7 Sep 2026 06:15:55 -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
  {i32,f64} on x86-64 returned 16 bytes instead of 12).
- Tail padding: the field loop omitted the final alignTo(size, align),
  so {i32,i8} returned 5 bytes instead of 8.

Fix the TRANSFER intrinsic inline path (genTransfer) to compare sizes using
fir::getTypeSizeAndAlignment, which includes tail padding for RecordType to
match the allocation extent used by Fortran STORAGE_SIZE, descriptor element
extent, and the runtime TRANSFER path.

For RecordType sources where sourceAlign < resultAlign (e.g. {i32,i8} with
4-byte alignment transferred to integer(8) with 8-byte alignment), copy via
fir.copy (a non-overlapping byte copy / memcpy of the full allocation extent)
into a result-aligned alloca instead of loading directly from the source
pointer. This avoids UB from over-aligned loads and preserves both inter-field
and tail padding bytes, satisfying 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). In the SystemZ and PPC64le
examples the record layout already included the padding, but the size query
used for argument classification was wrong. 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.
---
 flang/docs/ReleaseNotes.md                    | 54 +++++++++++
 .../include/flang/Optimizer/Dialect/FIRType.h | 29 +++++-
 flang/lib/Optimizer/Builder/IntrinsicCall.cpp | 65 ++++++++++++--
 flang/lib/Optimizer/Dialect/FIROps.cpp        |  8 +-
 flang/lib/Optimizer/Dialect/FIRType.cpp       | 21 +++++
 flang/test/Fir/CUDA/cuda-constructor-2.f90    | 72 +++++++++++++++
 flang/test/Fir/CUDA/cuda-shared-offset.mlir   | 44 +++++++++
 .../acc-fir-map-info-prep-privatize.mlir      |  7 +-
 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 | 29 ++++++
 .../Intrinsics/transfer-rec-tail-pad.f90      | 89 +++++++++++++++++++
 .../test/Transforms/allocation-placement.fir  | 44 +++++++++
 14 files changed, 551 insertions(+), 16 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 76a6b95dd3ecc..ac302d7e75f61 100644
--- a/flang/docs/ReleaseNotes.md
+++ b/flang/docs/ReleaseNotes.md
@@ -31,6 +31,60 @@ page](https://llvm.org/releases/).
 
 ## Bug Fixes
 
+- Fixed `fir::getTypeSizeAndAlignment` returning the wrong allocation size for
+  **packed `fir::RecordType`s** (produced by the AIX lowering of `BIND(C)`
+  derived types, or declared directly in textual FIR). Fields in a packed
+  record are placed back-to-back using each component's allocation size
+  (`alignTo(storeSize, ABIalign)`), not its raw store size, and the record's
+  ABI alignment is 1. For example, a packed `{i32, f64}` on x86-64 now
+  correctly reports 12 bytes instead of 16.
+  ([#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:
+  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`, and `{i16, i8}` (4 bytes) as `i32`, 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 a **`BIND(C)` / `VALUE` argument-passing ABI bug** on PPC64le:
+  derived types were classified using the unpadded store size rather than the
+  allocation size, producing the wrong number of GPR slots. The argument was
+  already passed by value; only the slot count was wrong. For example,
+  `{f128, i8}` (allocation size 32 bytes) is now correctly passed as
+  `[4 x i64]` instead of `[3 x i64]`, 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 sizes using
+  `getTypeSizeAndAlignment` (which includes tail padding for `RecordType`,
+  matching the allocation extent used by `STORAGE_SIZE` and the runtime
+  `TRANSFER` path). For `RecordType` sources, the inline path now copies
+  the record data via `fir.copy` into result-aligned storage when the source
+  alignment is less than the result alignment, preserving both internal and
+  tail padding bytes and 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..4fd9638e60d46 100644
--- a/flang/include/flang/Optimizer/Dialect/FIRType.h
+++ b/flang/include/flang/Optimizer/Dialect/FIRType.h
@@ -555,7 +555,34 @@ 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.
+///
+/// - **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
diff --git a/flang/lib/Optimizer/Builder/IntrinsicCall.cpp b/flang/lib/Optimizer/Builder/IntrinsicCall.cpp
index 9408dc213e2e1..2bdbad156c0b0 100644
--- a/flang/lib/Optimizer/Builder/IntrinsicCall.cpp
+++ b/flang/lib/Optimizer/Builder/IntrinsicCall.cpp
@@ -8999,22 +8999,69 @@ IntrinsicLibrary::genTransfer(mlir::Type resultType,
         (fir::isa_trivial(sourceType) ||
          mlir::isa<fir::RecordType>(sourceType)) &&
         fir::isa_trivial(moldType)) {
+      // Compare sizes from getTypeSizeAndAlignment. For RecordType, this
+      // includes tail padding to match the allocation extent used by
+      // STORAGE_SIZE and the TRANSFER runtime path. Alignment is handled
+      // separately: 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());
       auto resultSizeAndAlign = fir::getTypeSizeAndAlignment(
           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);
         }
-        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. {i32,i8} is 4-byte aligned
+        // while integer(8) requires 8-byte alignment), loading resultType
+        // directly from sourceBase would assert an over-aligned address and
+        // produce undefined behaviour.  In that case, copy the allocation-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
+        // allocation size, including tail padding).  Inter-field and tail
+        // padding bytes of the record are preserved, matching the runtime copy
+        // width and 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::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 5409b1c2da090..c0be8756bc592 100644
--- a/flang/lib/Optimizer/Dialect/FIROps.cpp
+++ b/flang/lib/Optimizer/Dialect/FIROps.cpp
@@ -1382,9 +1382,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 5f0dd68aa9396..297b43ed78f84 100644
--- a/flang/lib/Optimizer/Dialect/FIRType.cpp
+++ b/flang/lib/Optimizer/Dialect/FIRType.cpp
@@ -1664,6 +1664,25 @@ 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); // alloc size per field
+      }
+      return std::pair{size, static_cast<unsigned short>(1)};
+    }
     for (auto component : recTy.getTypeList()) {
       auto result = getTypeSizeAndAlignment(loc, component.second, dl, kindMap);
       if (!result)
@@ -1673,6 +1692,8 @@ 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.
+    size = llvm::alignTo(size, align);
     return std::pair{size, align};
   }
   if (auto logical = mlir::dyn_cast<fir::LogicalType>(ty)) {
diff --git a/flang/test/Fir/CUDA/cuda-constructor-2.f90 b/flang/test/Fir/CUDA/cuda-constructor-2.f90
index 11ab6a5a79ec1..84ada65887e1e 100644
--- a/flang/test/Fir/CUDA/cuda-constructor-2.f90
+++ b/flang/test/Fir/CUDA/cuda-constructor-2.f90
@@ -358,3 +358,75 @@ 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-LABEL: fir.global @_QMtestEtp_dev
+// NOUNIFIED: llvm.func internal @__cudaFortranConstructor() {
+// NOUNIFIED-DAG: %[[TPDEV:.*]] = fir.address_of(@_QMtestEtp_dev) : !fir.ref<!fir.type<tp{a:i32,b:i8}>>
+// NOUNIFIED-DAG: %[[TPDEV2:.*]] = fir.convert %[[TPDEV]] : (!fir.ref<!fir.type<tp{a:i32,b:i8}>>) -> !fir.ref<i8>
+// NOUNIFIED-DAG: %[[SZ8:.*]] = arith.constant 8 : index
+// NOUNIFIED-DAG: %[[SZ8I64:.*]] = fir.convert %[[SZ8]] : (index) -> i64
+// NOUNIFIED-DAG: fir.call @_FortranACUFRegisterVariable(%{{.*}}, %[[TPDEV2]], %{{.*}}, %[[SZ8I64]])
+// 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-LABEL: fir.global @_QMtestEtp_packed_dev
+// NOUNIFIED: llvm.func internal @__cudaFortranConstructor() {
+// NOUNIFIED-DAG: %[[TPPKDEV:.*]] = fir.address_of(@_QMtestEtp_packed_dev) : !fir.ref<!fir.type<tp<{i:i32,d:f64}>>>
+// NOUNIFIED-DAG: %[[TPPKDEV2:.*]] = fir.convert %[[TPPKDEV]] : (!fir.ref<!fir.type<tp<{i:i32,d:f64}>>>) -> !fir.ref<i8>
+// NOUNIFIED-DAG: %[[SZ12:.*]] = arith.constant 12 : index
+// NOUNIFIED-DAG: %[[SZ12I64:.*]] = fir.convert %[[SZ12]] : (index) -> i64
+// NOUNIFIED-DAG: fir.call @_FortranACUFRegisterVariable(%{{.*}}, %[[TPPKDEV2]], %{{.*}}, %[[SZ12I64]])
+// 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/OpenACC/acc-fir-map-info-prep-privatize.mlir b/flang/test/Fir/OpenACC/acc-fir-map-info-prep-privatize.mlir
index 4f5d9dd2b44cd..9c0395f01d6ef 100644
--- a/flang/test/Fir/OpenACC/acc-fir-map-info-prep-privatize.mlir
+++ b/flang/test/Fir/OpenACC/acc-fir-map-info-prep-privatize.mlir
@@ -51,15 +51,16 @@ func.func @private_without_par_dims() {
 
 // -----
 
-// A record element uses the padded stride: real(8) + real(4) has a size of 12
-// and an alignment of 8, so consecutive elements are 16 bytes apart.
+// A record element uses the padded stride: real(8) + real(4) has a stored
+// representation of 12 bytes and an alignment of 8, so consecutive elements
+// are 16 bytes apart (allocation extent rounded up to alignment).
 
 // CHECK-LABEL: func.func @private_static_record
 // CHECK: %[[PRIV:.*]] = acc.privatize
 // CHECK: %[[SIZE:.*]] = arith.constant 128 : i64
 // CHECK: acc.map_info varPtr(%[[PRIV]]
 // CHECK-SAME: size(%[[SIZE]] : i64)
-// CHECK-SAME: elementSize(12)
+// CHECK-SAME: elementSize(16)
 // CHECK-SAME: mapFlags(private)
 func.func @private_static_record() {
   %priv = acc.privatize par_dims(#acc<par_dims[]>)
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..989eea5590972
--- /dev/null
+++ b/flang/test/Lower/Intrinsics/transfer-rec-tail-pad-llvm.f90
@@ -0,0 +1,29 @@
+! 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 5-byte stored representation and 8-byte allocation
+! extent (including 3 bytes of tail padding).  TRANSFER to integer(8) must copy
+! all 8 bytes into result-aligned storage before loading the result.
+module m
+  type :: t
+    integer(4) :: a
+    integer(1) :: b
+  end type
+end module
+
+subroutine transfer_rec_to_int8(out)
+  use m
+  type(t) :: src
+  integer(8) :: out
+  src%a = 42
+  src%b = 7_1
+  out = transfer(src, out)
+end subroutine
+
+! CHECK-LABEL: define{{.*}} @transfer_rec_to_int8_(
+! CHECK-DAG:   %[[TMP:.*]] = alloca i64{{.*}}, align 8
+! CHECK-DAG:   %[[SRC:.*]] = alloca %_QMmTt,
+! CHECK:       call void @llvm.memcpy.p0.p0.i64(ptr %[[TMP]], ptr %[[SRC]], i64 8, i1 false)
+! CHECK:       %[[RESULT:.*]] = load i64, ptr %[[TMP]], align 8
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..d7f5b148baaa4
--- /dev/null
+++ b/flang/test/Lower/Intrinsics/transfer-rec-tail-pad.f90
@@ -0,0 +1,89 @@
+! 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 sizes from getTypeSizeAndAlignment.
+! For derived types, this includes tail padding, matching the allocation extent
+! used by STORAGE_SIZE and the TRANSFER runtime path.
+!
+! All checks are at the HLFIR level; LLVM IR lowering is covered by transfer-rec-tail-pad-llvm.f90.
+
+! Shape 1: tail-padded record whose allocation size matches integer(8).
+!   t1 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, align = 8B.
+!   8 == 8 on allocation size -> INLINES via fir.copy into an 8-byte aligned alloca.
+!   fir.copy copies the full 8 bytes (including 3 tail-padding bytes), matching
+!   the runtime copy width and preserving physical representation.
+module m1
+  type :: t1
+    integer(4) :: a
+    integer(1) :: b
+  end type
+end module
+
+! Shape 2: tail-padded record whose allocation size (16B) does not match real(10) store size (10B).
+!   t2 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, align = 16B.
+!   16 != 10 on size comparison -> stays on RUNTIME path (_FortranATransfer).
+module m2
+  type :: t2
+    integer(8) :: a
+    integer(2) :: b
+  end type
+end module
+
+subroutine transfer_rec_to_int8(res)
+  ! CHECK-LABEL: func @_QPtransfer_rec_to_int8(
+  ! CHECK-NOT:     fir.call @_FortranATransfer
+  ! CHECK:         %[[TMP:.*]] = fir.alloca i64
+  ! CHECK:         %[[SRC_BYTES:.*]] = fir.convert {{.*}} : (!fir.ref<!fir.type<{{.*}}>>) -> !fir.ref<!fir.array<8xi8>>
+  ! CHECK:         %[[DST_BYTES:.*]] = fir.convert %[[TMP]] : (!fir.ref<i64>) -> !fir.ref<!fir.array<8xi8>>
+  ! CHECK:         fir.copy %[[SRC_BYTES]] to %[[DST_BYTES]] no_overlap : !fir.ref<!fir.array<8xi8>>, !fir.ref<!fir.array<8xi8>>
+  ! CHECK:         fir.load %[[TMP]] : !fir.ref<i64>
+  ! CHECK:         return
+  use m1
+  type(t1) :: x
+  integer(8) :: res
+  x%a = 1
+  x%b = 2_1
+  res = transfer(x, res)
+end subroutine
+
+subroutine transfer_rec_to_real10(out)
+  ! CHECK-LABEL: func @_QPtransfer_rec_to_real10(
+  ! CHECK:         fir.call @_FortranATransfer
+  ! CHECK-NOT:     fir.load {{.*}} : !fir.ref<f80>
+  ! CHECK:         return
+  use m2
+  type(t2) :: src
+  real(10) :: out
+  src%a = 42
+  src%b = 7
+  out = transfer(src, out)
+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.
+! This test covers HLFIR data flow only: the RUN line emits HLFIR and does
+! not execute the subroutine, so correctness of the byte values is not
+! verified here.
+subroutine transfer_bindc_record_to_int128(res)
+  ! CHECK-LABEL: func @_QPtransfer_bindc_record_to_int128(
+  ! CHECK:         %[[TMP128:.*]] = fir.alloca i128
+  ! CHECK:         %[[SRC_BYTES128:.*]] = fir.convert {{.*}} : (!fir.ref<!fir.type<{{.*}}>>) -> !fir.ref<!fir.array<16xi8>>
+  ! CHECK:         %[[DST_BYTES128:.*]] = fir.convert %[[TMP128]] : (!fir.ref<i128>) -> !fir.ref<!fir.array<16xi8>>
+  ! CHECK:         fir.copy %[[SRC_BYTES128]] to %[[DST_BYTES128]] no_overlap : !fir.ref<!fir.array<16xi8>>, !fir.ref<!fir.array<16xi8>>
+  ! CHECK:         fir.load %[[TMP128]] : !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
+  res = transfer(source, res)
+end subroutine
diff --git a/flang/test/Transforms/allocation-placement.fir b/flang/test/Transforms/allocation-placement.fir
index ec70af24e5d13..aa09d830fd5a5 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