[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 11 06:51:33 PDT 2026
https://github.com/DanielCChen updated https://github.com/llvm/llvm-project/pull/220377
>From 87e92b91109df92f3be6611c338db252fd829a14 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.
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, correcting two path-selection errors:
inlining when store sizes differ (loading the wrong number of bytes) and
falling back to the runtime when store sizes match (missed inlining).
Keeping representation width separate from allocation extent preserves the
previous inline/runtime choices for unpacked TRANSFER cases where widths
differ from allocation sizes (e.g. real(10) with store size 10, allocation
size 16).
For RecordType sources where sourceAlign < resultAlign, copy via fir.copy
into a result-aligned alloca instead of a direct load. Equal byte widths
don't guarantee sufficient address alignment for the result load. Using an
aggregate store would discard inter-field padding even in an aligned
temporary, so a bytewise copy is required to satisfy 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 | 55 +++++++++++
.../include/flang/Optimizer/Dialect/FIRType.h | 41 ++++++++-
flang/lib/Optimizer/Builder/IntrinsicCall.cpp | 71 +++++++++++---
flang/lib/Optimizer/Dialect/FIROps.cpp | 8 +-
flang/lib/Optimizer/Dialect/FIRType.cpp | 69 ++++++++++++--
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 | 92 +++++++++++++++++++
.../test/Transforms/allocation-placement.fir | 44 +++++++++
14 files changed, 611 insertions(+), 26 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..22763addcdd5e 100644
--- a/flang/docs/ReleaseNotes.md
+++ b/flang/docs/ReleaseNotes.md
@@ -31,6 +31,61 @@ 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 **stored-representation
+ widths** (excluding outer tail padding) rather than allocation sizes when
+ deciding whether to inline a load instead of calling the runtime. This
+ corrects two path-selection errors: inlining when the store sizes differ
+ (which loaded the wrong number of bytes) and falling back to the runtime
+ when the store sizes match (a missed-inlining regression). 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..66a5be63c6c35 100644
--- a/flang/include/flang/Optimizer/Dialect/FIRType.h
+++ b/flang/include/flang/Optimizer/Dialect/FIRType.h
@@ -555,7 +555,46 @@ 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 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);
+
+/// 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
diff --git a/flang/lib/Optimizer/Builder/IntrinsicCall.cpp b/flang/lib/Optimizer/Builder/IntrinsicCall.cpp
index a139af9a29e57..234e9610f5103 100644
--- a/flang/lib/Optimizer/Builder/IntrinsicCall.cpp
+++ b/flang/lib/Optimizer/Builder/IntrinsicCall.cpp
@@ -8972,22 +8972,71 @@ 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;
+ // the comparison selects the inline path by 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);
}
- 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::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 5f0dd68aa9396..fd69863ae9361 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)) {
@@ -1653,7 +1654,8 @@ fir::getTypeSizeAndAlignment(mlir::Location loc, mlir::Type ty,
// Dynamic / unknown shapes have no compile-time byte size.
if (seqTy.hasDynamicExtents() || seqTy.hasUnknownShape())
return std::nullopt;
- 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;
@@ -1664,8 +1666,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;
@@ -1673,18 +1704,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;
@@ -1695,6 +1732,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..bc94189177d2f 100644
--- a/flang/test/Fir/CUDA/cuda-constructor-2.f90
+++ b/flang/test/Fir/CUDA/cuda-constructor-2.f90
@@ -351,3 +351,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 54953870331f0..a6e7350c03025 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..3d8358263024f
--- /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 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-DAG: %[[TMP:.*]] = alloca x86_fp80{{.*}}, align 16
+! CHECK-DAG: %[[SRC:.*]] = alloca %_QMmTt,
+! CHECK: call void @llvm.memcpy.p0.p0.i64(ptr %[[TMP]], ptr %[[SRC]], 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..b2d46e92b9d69
--- /dev/null
+++ b/flang/test/Lower/Intrinsics/transfer-rec-tail-pad.f90
@@ -0,0 +1,92 @@
+! 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.
+!
+! 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 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
+! despite the stored-representation width mismatch (t2 is 5B, integer(8) is 8B).
+! With the fix: 5 != 8 on store size -> stays on RUNTIME path.
+! (The runtime copies the full 8-byte allocation extent either way;
+! representation-width mismatch, not tail-padding avoidance, selects the 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: %[[SRC_BYTES:.*]] = fir.convert {{.*}} : (!fir.ref<!fir.type<{{.*}}>>) -> !fir.ref<!fir.array<10xi8>>
+ ! CHECK: %[[DST_BYTES:.*]] = fir.convert %[[TMP]] : (!fir.ref<f80>) -> !fir.ref<!fir.array<10xi8>>
+ ! CHECK: fir.copy %[[SRC_BYTES]] to %[[DST_BYTES]] no_overlap : !fir.ref<!fir.array<10xi8>>, !fir.ref<!fir.array<10xi8>>
+ ! CHECK: fir.load %[[TMP]] : !fir.ref<f80>
+ ! CHECK: return
+ use m1
+ type(t1) :: src
+ real(10) :: out
+ src%a = 42
+ src%b = 7
+ out = transfer(src, out)
+end subroutine
+
+subroutine transfer_rec_to_int8(res)
+ ! CHECK-LABEL: func @_QPtransfer_rec_to_int8(
+ ! CHECK: fir.call @_FortranATransfer
+ ! CHECK-NOT: fir.load {{.*}} : !fir.ref<i64>
+ ! CHECK: return
+ use m2
+ type(t2) :: x
+ integer(8) :: res
+ x%a = 1
+ x%b = 2_1
+ res = transfer(x, res)
+end subroutine
+! 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 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