[flang-commits] [flang] [flang] Fix `getTypeSizeAndAlignment` for packed/tail-padded `RecordType` and `TRANSFER` gate (PR #220377)
Daniel Chen via flang-commits
flang-commits at lists.llvm.org
Fri Sep 4 09:06:57 PDT 2026
https://github.com/DanielCChen updated https://github.com/llvm/llvm-project/pull/220377
>From 508fca0615d695bd3af67813d9258c125861d200 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 a5705fe18983c..ea00d0f3f6394 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