[flang-commits] [flang] [llvm] [flang-rt] - Lightweight runtime assignment function (AssignSimple) for intrinsic-type assignments (PR #213704)
Pranav Bhandarkar via flang-commits
flang-commits at lists.llvm.org
Mon Aug 3 08:48:06 PDT 2026
https://github.com/bhandarkar-pranav created https://github.com/llvm/llvm-project/pull/213704
This PR introduces a lightweight assignment runtime path (`_FortranAAssignSimple`) for intrinsic-type arrays with the goal of reducing compile-time overhead seen primarily in the form of severly increased time taken by LTO. This PR includes only the changes to the runtime (flang-rt) and as such just with this PR compile-time improvements will not be visible.
**Problem**
When compiling Fortran code with OpenMP GPU offload and `firstprivate(allocatable_array)`, LLVM's Attributor creates excessive abstract attributes analyzing complex runtime assignment machinery:
**Symptom:**
- **Test case:** 8-element allocatable integer array with `firstprivate` clause
- **Compile time:** 24.97s (vs 0.78s for `private` - **32x slower**)
- **Root cause:** LLVM Attributor analyzing complex Fortran runtime functions
**Why this happens:**
1. `firstprivate` requires copying arrays from host to device
2. Flang generates call to `_FortranAAssign(to_device, from_host)`
3. LTO pulls in 177 runtime functions from `libflang_rt.runtime.a`
4. OpenMPOpt/Attributor analyzes all 177 functions, creating **1,041,950 abstract attributes**
5. Time spent in OpenMPOpt: **9.75s (39% of total compile time)**
**The core issue:** `_FortranAAssign` handles ALL Fortran assignment cases (scalar, array, polymorphic, character, derived type, user-defined assignment, aliasing detection, finalization) with **999 basic blocks** in a single function. For a trivial integer array copy, this forces the optimizer to analyze machinery it will never execute.
>From Attributor debug output:
```
[Attributor] Update: [AAIsDead] for ... at position {fn:_FortranAAssign}
with state Live[#BB 1/999][#TBEP 1][#KDE 0]
^^^
999 basic blocks in ONE function!
```
**Overhead:**
- **Actually executed at runtime:** ~5-10 functions, ~200 basic blocks
- **Analyzed at compile-time:** 177 functions, ~1800 basic blocks
- **Overhead:** **17x-35x more code analyzed than executed**
Intrinsic types never have dynamic components requiring deferred operations, so the `WorkQueue` in `_FortranAAssign` is not really needed. Therefore, we split the Fortran assignment runtime API based on statically known information:
**1. `_FortranAAssignSimple` (NEW) - Fast Path**
- Handles intrinsic type arrays (integer, real, complex, logical)
- Single `memmove()` for contiguous, element-wise loop for non-contiguous
- Minimal LTO pull-in (~3-4 functions vs 177)
- Runtime checks verify correct usage
**2. `_FortranAAssign` (EXISTING) - Complex Path**
- Handles derived types, polymorphic, character, user-defined assignment
- Retains full WorkQueue, finalization, aliasing detection machinery
- Only called when actually needed
`_FortranAAssignSimple` is used when ALL conditions are true:
1. Intrinsic element type (not derived type)
2. Matching ranks (no scalar-to-array broadcasting)
3. Non-volatile
4. Not polymorphic
5. Not explicit-length character
6. Not temporary LHS
This is a part of the fix for https://github.com/llvm/llvm-project/issues/203915
>From 22e413e73b4466cec6c20ea8bf5533e1370fd978 Mon Sep 17 00:00:00 2001
From: Pranav Bhandarkar <pranav.bhandarkar at amd.com>
Date: Sat, 1 Aug 2026 15:56:35 -0500
Subject: [PATCH] [flang-rt] - Lightweight runtime assignment function
(AssignSimple) for intrinsic-type assignments.
This PR introduces a lightweight assignment runtime path (`_FortranAAssignSimple`) for intrinsic-type arrays
with the goal of reducing compile-time overhead seen primarily in the form of severly increased time taken by LTO.
This PR includes only the changes to the runtime (flang-rt) and as such just with this PR compile-time improvements
will not be visible.
**Problem**
When compiling Fortran code with OpenMP GPU offload and `firstprivate(allocatable_array)`, LLVM's Attributor creates excessive abstract attributes analyzing complex runtime assignment machinery:
**Symptom:**
- **Test case:** 8-element allocatable integer array with `firstprivate` clause
- **Compile time:** 24.97s (vs 0.78s for `private` - **32x slower**)
- **Root cause:** LLVM Attributor analyzing complex Fortran runtime functions
**Why this happens:**
1. `firstprivate` requires copying arrays from host to device
2. Flang generates call to `_FortranAAssign(to_device, from_host)`
3. LTO pulls in 177 runtime functions from `libflang_rt.runtime.a`
4. OpenMPOpt/Attributor analyzes all 177 functions, creating **1,041,950 abstract attributes**
5. Time spent in OpenMPOpt: **9.75s (39% of total compile time)**
**The core issue:** `_FortranAAssign` handles ALL Fortran assignment cases (scalar, array, polymorphic, character, derived type, user-defined assignment, aliasing detection, finalization) with **999 basic blocks** in a single function. For a trivial integer array copy, this forces the optimizer to analyze machinery it will never execute.
>From Attributor debug output:
```
[Attributor] Update: [AAIsDead] for ... at position {fn:_FortranAAssign}
with state Live[#BB 1/999][#TBEP 1][#KDE 0]
^^^
999 basic blocks in ONE function!
```
**Overhead:**
- **Actually executed at runtime:** ~5-10 functions, ~200 basic blocks
- **Analyzed at compile-time:** 177 functions, ~1800 basic blocks
- **Overhead:** **17x-35x more code analyzed than executed**
Intrinsic types never have dynamic components requiring deferred operations, so the `WorkQueue` in `_FortranAAssign` is not really needed.
Therefore, we split the Fortran assignment runtime API based on statically known information:
**1. `_FortranAAssignSimple` (NEW) - Fast Path**
- Handles intrinsic type arrays (integer, real, complex, logical)
- Single `memmove()` for contiguous, element-wise loop for non-contiguous
- Minimal LTO pull-in (~3-4 functions vs 177)
- Runtime checks verify correct usage
**2. `_FortranAAssign` (EXISTING) - Complex Path**
- Handles derived types, polymorphic, character, user-defined assignment
- Retains full WorkQueue, finalization, aliasing detection machinery
- Only called when actually needed
`_FortranAAssignSimple` is used when ALL conditions are true:
1. Intrinsic element type (not derived type)
2. Matching ranks (no scalar-to-array broadcasting)
3. Non-volatile
4. Not polymorphic
5. Not explicit-length character
6. Not temporary LHS
This is a part of the fix for https://github.com/llvm/llvm-project/issues/203915
---
flang-rt/lib/runtime/assign.cpp | 214 ++++++++++++++++++++++++++
flang-rt/lib/runtime/tools.cpp | 1 +
flang-rt/unittests/Runtime/Assign.cpp | 151 ++++++++++++++++++
flang/include/flang/Runtime/assign.h | 4 +
4 files changed, 370 insertions(+)
diff --git a/flang-rt/lib/runtime/assign.cpp b/flang-rt/lib/runtime/assign.cpp
index 0d0710382a055..2de590abd4316 100644
--- a/flang-rt/lib/runtime/assign.cpp
+++ b/flang-rt/lib/runtime/assign.cpp
@@ -10,6 +10,7 @@
#include "flang-rt/runtime/assign-impl.h"
#include "flang-rt/runtime/derived.h"
#include "flang-rt/runtime/descriptor.h"
+#include "flang-rt/runtime/memory.h"
#include "flang-rt/runtime/stat.h"
#include "flang-rt/runtime/terminator.h"
#include "flang-rt/runtime/tools.h"
@@ -851,6 +852,219 @@ void RTDEF(AssignExplicitLengthCharacter)(Descriptor &to,
ExplicitLengthCharacterLHS);
}
+void RTDEF(AssignSimple)(Descriptor &to, const Descriptor &from,
+ const char *sourceFile, int sourceLine) {
+ Terminator terminator{sourceFile, sourceLine};
+ // AssignSimple: fast path for intrinsic type assignments (integer, real,
+ // complex, logical). The compiler routes here only when:
+ // - LHS element type is trivial (isa_trivial), not derived/polymorphic
+ // - LHS and RHS ranks match (no scalar-to-array broadcasting)
+ // - LHS is not volatile (volatile needs memory ordering semantics)
+
+ if (to.rank() != from.rank()) {
+ terminator.Crash("AssignSimple: rank mismatch (to.rank=%d, from.rank=%d)",
+ to.rank(), from.rank());
+ }
+ if (to.ElementBytes() != from.ElementBytes()) {
+ terminator.Crash(
+ "AssignSimple: ElementBytes mismatch (to.ElementBytes=%d, "
+ "from.ElementBytes=%d)",
+ to.ElementBytes(), from.ElementBytes());
+ }
+ if (to.type().IsDerived()) {
+ terminator.Crash("AssignSimple: Cannot assign to derived type");
+ }
+
+ std::size_t elementBytes{to.ElementBytes()};
+ std::size_t elements{from.Elements()};
+
+ // Conformability check for non-allocatable arrays.
+ // 1. For allocatable LHS, shape mismatch triggers reallocation (handled in
+ // Step 2 below).
+ // 2. For non-allocatable LHS, shape mismatch is an error per Fortran
+ // 2018 10.2.1.2 -- the shapes must conform. This matches the
+ // conformability check in AssignTicket::Begin().
+ //
+ // Example: x(8:1:-3) = x(5:2:-2) where x is not allocatable and LHS has 3
+ // elements, RHS has 2.
+ if (!to.IsAllocatable() && from.rank() > 0) {
+ std::size_t toElements{to.Elements()};
+ if (toElements != elements) {
+ terminator.Crash("AssignSimple: mismatching element counts in "
+ "non-allocatable array assignment (to %zd, from %zd)",
+ toElements, elements);
+ }
+ }
+
+ // Step 1: Aliasing detection.
+ //
+ // When LHS and RHS reference overlapping memory (e.g., a(9:5:-1) = a(1:5:1)),
+ // an element-wise copy can overwrite source elements before they are read.
+ //
+ // Example of data corruption without temporary:
+ // integer, dimension(3, 2) :: a
+ // a = reshape((/1, 2, 3, 4, 5, 6/), (/3, 2/))
+ // a = a(3:1:-1, 2:1:-1) ! reverse both dimensions
+ //
+ // The element-wise loop iterates in column-major order for both LHS and
+ // RHS:
+ // Iter 1: a(1,1) = a(3,2) = 6 -> overwrites a(1,1), was 1
+ // Iter 2: a(2,1) = a(2,2) = 5 -> overwrites a(2,1), was 2
+ // Iter 3: a(3,1) = a(1,2) = 4 -> overwrites a(3,1), was 3
+ // Iter 4: a(1,2) = a(3,1) -> reads 4, but expected 3 (WRONG!)
+ // Iter 5: a(2,2) = a(2,1) -> reads 5, but expected 2 (WRONG!)
+ // Iter 6: a(3,2) = a(1,1) -> reads 6, but expected 1 (WRONG!)
+ // Result: (/6,5,4,4,5,6/) instead of (/6,5,4,3,2,1/)
+ //
+ // To fix this, we copy the RHS data into a contiguous temporary buffer
+ // before modifying the LHS. The temp preserves the original source values.
+ //
+ // MayAlias() checks whether the memory ranges described by the two
+ // descriptors overlap. It uses MaximalByteOffsetRange() to compute the
+ // byte extent of each descriptor (accounting for negative strides), then
+ // checks if those ranges overlap via RangesOverlap(). All three are static
+ // functions in this file -- calling them from AssignSimple adds zero
+ // additional LTO pull-in.
+ //
+ // When aliasing is detected, we always create a temporary, even if both
+ // sides are contiguous (where memmove would handle overlap correctly).
+ // This keeps the code simple and covers the case where an allocatable LHS
+ // needs reallocation: deallocating the LHS would free the RHS's backing
+ // memory if they alias.
+ // Example: integer, allocatable :: a(:)
+ // allocate(a(5)); a = [1,2,3,4,5]
+ // a = a(1:3) ! shapes differ -> deallocate a -> frees a(1:3)'s
+ // data
+ //
+ // TODO: Refining the condition for creating a temporary buffer.
+ // For better performance on contiguous aliased assignments that do not
+ // require reallocation, we could refine the condition to only create a temp
+ // when:
+ // (needsReallocation || !to.IsContiguous() || !from.IsContiguous())
+ // where
+ // needsReallocation = (to.isAllocatable() && (!to.isAllocated ||
+ // shape_mismatch))
+ // For needsReallocation, see Step 2 below.
+ // Right now though, the simpler approach of always creating a temporary
+ // when aliasing is detected is fine.
+ //
+ // The temporary buffer is allocated via AllocateMemoryOrCrash(), which is
+ // a thin wrapper around std::malloc. This is GPU-safe: both
+ // AllocateMemoryOrCrash and std::malloc are available in GPU device code
+ // (via the device-side heap allocator), and Assign already calls
+ // Descriptor::Allocate() which goes through the same std::malloc path.
+ char *tempBuffer{nullptr};
+ if (MayAlias(to, from)) {
+ std::size_t totalBytes{elements * elementBytes};
+ tempBuffer =
+ static_cast<char *>(AllocateMemoryOrCrash(terminator, totalBytes));
+ // Copy from's data into the contiguous temp buffer, element by element.
+ // This handles non-contiguous RHS (e.g., strided slices) by walking
+ // the descriptor's subscripts.
+ if (from.IsContiguous()) {
+ runtime::memcpy(tempBuffer, from.OffsetElement(), totalBytes);
+ } else {
+ SubscriptValue fromAt[maxRank];
+ from.GetLowerBounds(fromAt);
+ char *tempAt{tempBuffer};
+ for (std::size_t n{elements}; n-- > 0;
+ from.IncrementSubscripts(fromAt), tempAt += elementBytes) {
+ runtime::memcpy(tempAt, from.Element<const char>(fromAt), elementBytes);
+ }
+ }
+ }
+
+ // Step 2: Handle allocation/reallocation for allocatable LHS.
+ //
+ // This must come AFTER the aliasing check above. That is because if we
+ // must call deallocate the LHS (If LHS and RHS alias and shapes differ), the
+ // Deallocate() call below would free the memory that the RHS points to. The
+ // temporary created in Step 1 preserves the RHS data, making the deallocation
+ // safe.
+ //
+ // Per Fortran 2018 10.2.1.3(3): for allocatable LHS, if the LHS is
+ // already allocated and shapes differ, it must be deallocated and
+ // reallocated to match the RHS shape.
+ if (to.IsAllocatable()) {
+ bool needsReallocation{false};
+
+ if (!to.IsAllocated()) {
+ needsReallocation = true;
+ } else if (from.rank() > 0) {
+ int rank{to.rank()};
+ for (int j{0}; j < rank; ++j) {
+ if (to.GetDimension(j).Extent() != from.GetDimension(j).Extent()) {
+ needsReallocation = true;
+ break;
+ }
+ }
+ }
+
+ if (needsReallocation) {
+ if (to.IsAllocated()) {
+ to.Deallocate();
+ }
+ to.raw().elem_len = elementBytes;
+ int rank{to.rank()};
+ auto stride{static_cast<SubscriptValue>(elementBytes)};
+ for (int j{0}; j < rank; ++j) {
+ const auto &fromDim{from.GetDimension(j)};
+ auto &toDim{to.GetDimension(j)};
+ toDim.SetBounds(fromDim.LowerBound(), fromDim.UpperBound());
+ toDim.SetByteStride(stride);
+ stride *= toDim.Extent();
+ }
+ int stat{to.Allocate(kNoAsyncObject)};
+ if (stat != StatOk) {
+ terminator.Crash("AssignSimple: allocation failed (stat=%d)", stat);
+ }
+ }
+ }
+
+ // Step 3: Copy data into LHS.
+ //
+ // If we created a temporary in Step 1 (aliasing detected), copy from
+ // the contiguous temp buffer. Otherwise, copy directly from the RHS.
+ if (tempBuffer) {
+ // Source is the contiguous temp buffer. Destination (LHS) may or may
+ // not be contiguous.
+ if (to.IsContiguous()) {
+ // Both temp (always contiguous) and LHS are contiguous: bulk copy.
+ runtime::memcpy(to.OffsetElement(), tempBuffer, elements * elementBytes);
+ } else {
+ // LHS is non-contiguous (e.g., strided section): element-wise copy
+ // from the contiguous temp buffer into LHS's strided layout.
+ SubscriptValue toAt[maxRank];
+ to.GetLowerBounds(toAt);
+ const char *tempAt{tempBuffer};
+ for (std::size_t n{elements}; n-- > 0;
+ to.IncrementSubscripts(toAt), tempAt += elementBytes) {
+ runtime::memcpy(to.Element<char>(toAt), tempAt, elementBytes);
+ }
+ }
+ FreeMemory(tempBuffer);
+ } else {
+ // No aliasing: copy directly from RHS to LHS.
+ if (to.IsContiguous() && from.IsContiguous()) {
+ // Both contiguous: memmove handles any incidental overlap safely.
+ runtime::memmove(
+ to.OffsetElement(), from.OffsetElement(), elements * elementBytes);
+ } else {
+ // At least one non-contiguous: element-wise copy.
+ // This handles strided slices, transformational intrinsic results, etc.
+ SubscriptValue toAt[maxRank];
+ to.GetLowerBounds(toAt);
+ SubscriptValue fromAt[maxRank];
+ from.GetLowerBounds(fromAt);
+ for (std::size_t n{elements}; n-- > 0;
+ to.IncrementSubscripts(toAt), from.IncrementSubscripts(fromAt)) {
+ runtime::memmove(to.Element<char>(toAt),
+ from.Element<const char>(fromAt), elementBytes);
+ }
+ }
+ }
+}
+
void RTDEF(AssignPolymorphic)(Descriptor &to, const Descriptor &from,
const char *sourceFile, int sourceLine) {
Terminator terminator{sourceFile, sourceLine};
diff --git a/flang-rt/lib/runtime/tools.cpp b/flang-rt/lib/runtime/tools.cpp
index 361e2469b4a89..b7408c81f83f4 100644
--- a/flang-rt/lib/runtime/tools.cpp
+++ b/flang-rt/lib/runtime/tools.cpp
@@ -205,6 +205,7 @@ RT_API_ATTRS void ShallowCopyInner(const Descriptor &to, const Descriptor &from,
template <typename P>
RT_API_ATTRS void ShallowCopyRank(const Descriptor &to, const Descriptor &from,
bool toIsContiguous, bool fromIsContiguous) {
+ INTERNAL_CHECK(to.rank() == from.rank());
// Specialize only common low ranks; use generic fallback for higher ranks
switch (to.rank()) {
case 1:
diff --git a/flang-rt/unittests/Runtime/Assign.cpp b/flang-rt/unittests/Runtime/Assign.cpp
index 4001cc90ca0a1..b1d017a784538 100644
--- a/flang-rt/unittests/Runtime/Assign.cpp
+++ b/flang-rt/unittests/Runtime/Assign.cpp
@@ -53,3 +53,154 @@ TEST(Assign, RTNAME(CopyInAssign)) {
intResultStrided.Destroy();
}
+
+TEST(AssignSimple, AliasedReverseStride) {
+ // Test aliasing detection with reverse-stride copy: a(5:1:-1) = a(1:5)
+ // This exercises the MayAlias() detection and temporary buffer path.
+ // Without temp buffer, the element-wise copy would corrupt data by
+ // overwriting source elements before they're read.
+
+ // Create backing storage as a C++ array
+ int data[5] = {1, 2, 3, 4, 5};
+ constexpr int elementBytes = sizeof(int);
+ TypeCode intType{TypeCategory::Integer, 4};
+
+ // Create source descriptor: forward view (1:5)
+ StaticDescriptor<1> staticSource;
+ Descriptor &source{staticSource.descriptor()};
+ SubscriptValue extent[1]{5};
+ source.Establish(intType, elementBytes, data, 1, extent);
+ source.GetDimension(0).SetLowerBound(1);
+
+ // Create dest descriptor: reverse view (5:1:-1) of same memory
+ StaticDescriptor<1> staticDest;
+ Descriptor &dest{staticDest.descriptor()};
+ dest.Establish(
+ intType, elementBytes, &data[4], 1, extent); // Start at last element
+ dest.GetDimension(0).SetLowerBound(1);
+ dest.GetDimension(0).SetByteStride(-elementBytes); // Negative stride
+
+ RTNAME(AssignSimple)(dest, source, __FILE__, __LINE__);
+
+ // Verify reverse copy succeeded.
+ // The backing array should now be [5,4,3,2,1] (reversed from [1,2,3,4,5])
+ int expected[5] = {5, 4, 3, 2, 1};
+ EXPECT_EQ(std::memcmp(data, expected, 5 * sizeof(int)), 0);
+}
+
+TEST(AssignSimple, ReallocateUnallocated) {
+ // Test allocatable reallocation from unallocated state
+ StaticDescriptor<1> staticDest;
+ Descriptor &dest{staticDest.descriptor()};
+ dest.Establish(TypeCode{TypeCategory::Integer, 4}, sizeof(int), nullptr, 1,
+ nullptr, CFI_attribute_allocatable);
+ dest.GetDimension(0).SetBounds(1, 0);
+ // dest is now unallocated
+
+ auto source{MakeArray<TypeCategory::Integer, 4>(
+ std::vector<int>{4}, std::vector<int>{10, 20, 30, 40}, sizeof(int))};
+
+ EXPECT_FALSE(dest.IsAllocated());
+
+ RTNAME(AssignSimple)(dest, *source, __FILE__, __LINE__);
+
+ // Verify dest is now allocated with correct shape and data
+ EXPECT_TRUE(dest.IsAllocated());
+ EXPECT_EQ(dest.rank(), 1);
+ EXPECT_EQ(dest.GetDimension(0).LowerBound(), 1);
+ EXPECT_EQ(dest.GetDimension(0).Extent(), 4);
+ EXPECT_EQ(dest.Elements(), 4);
+
+ int expected[4] = {10, 20, 30, 40};
+ EXPECT_EQ(
+ std::memcmp(dest.OffsetElement<int>(0), expected, 4 * sizeof(int)), 0);
+
+ // Verify source unchanged
+ EXPECT_EQ(
+ std::memcmp(source->OffsetElement<int>(0), expected, 4 * sizeof(int)), 0);
+
+ dest.Destroy();
+ source->Destroy();
+}
+
+TEST(AssignSimple, ReallocateShapeMismatch) {
+ // Test allocatable reallocation when shape (extent) differs
+ auto dest{MakeArray<TypeCategory::Integer, 4>(
+ std::vector<int>{3}, std::vector<int>{1, 2, 3}, sizeof(int))};
+
+ auto source{MakeArray<TypeCategory::Integer, 4>(
+ std::vector<int>{5}, std::vector<int>{10, 20, 30, 40, 50}, sizeof(int))};
+
+ EXPECT_TRUE(dest->IsAllocated());
+ EXPECT_EQ(dest->GetDimension(0).Extent(), 3);
+
+ RTNAME(AssignSimple)(*dest, *source, __FILE__, __LINE__);
+
+ // Verify dest was reallocated with new extent matching source
+ EXPECT_TRUE(dest->IsAllocated());
+ EXPECT_EQ(dest->rank(), 1);
+ EXPECT_EQ(dest->GetDimension(0).LowerBound(), 1);
+ EXPECT_EQ(dest->GetDimension(0).Extent(), 5);
+ EXPECT_EQ(dest->Elements(), 5);
+
+ int expected[5] = {10, 20, 30, 40, 50};
+ EXPECT_EQ(
+ std::memcmp(dest->OffsetElement<int>(0), expected, 5 * sizeof(int)), 0);
+
+ // Verify source unchanged
+ EXPECT_EQ(
+ std::memcmp(source->OffsetElement<int>(0), expected, 5 * sizeof(int)), 0);
+
+ dest->Destroy();
+ source->Destroy();
+}
+
+TEST(AssignSimple, NonContiguousToContiguous) {
+ // Test non-contiguous source (strided) to contiguous destination
+ // Pattern: take every other element from an 8-element array
+ auto source{MakeArray<TypeCategory::Integer, 4>(std::vector<int>{8},
+ std::vector<int>{1, 2, 3, 4, 5, 6, 7, 8}, sizeof(int))};
+
+ // Make source non-contiguous: stride=2*sizeof(int), extent=4
+ // This gives us elements [1, 3, 5, 7] from the backing array
+ source->GetDimension(0).SetByteStride(sizeof(int) * 2);
+ source->GetDimension(0).SetExtent(4);
+ EXPECT_FALSE(source->IsContiguous());
+
+ auto dest{MakeArray<TypeCategory::Integer, 4>(
+ std::vector<int>{4}, std::vector<int>{0, 0, 0, 0}, sizeof(int))};
+ EXPECT_TRUE(dest->IsContiguous());
+
+ RTNAME(AssignSimple)(*dest, *source, __FILE__, __LINE__);
+
+ // Verify dest has strided elements from source
+ int expected[4] = {1, 3, 5, 7};
+ EXPECT_EQ(
+ std::memcmp(dest->OffsetElement<int>(0), expected, 4 * sizeof(int)), 0);
+ EXPECT_TRUE(dest->IsContiguous());
+
+ dest->Destroy();
+ source->Destroy();
+}
+
+TEST(AssignSimple, ZeroSizeArray) {
+ // Test zero-size array edge case
+ auto source{MakeArray<TypeCategory::Integer, 4>(
+ std::vector<int>{0}, std::vector<int>{}, sizeof(int))};
+
+ auto dest{MakeArray<TypeCategory::Integer, 4>(
+ std::vector<int>{0}, std::vector<int>{}, sizeof(int))};
+
+ EXPECT_EQ(source->Elements(), 0);
+ EXPECT_EQ(dest->Elements(), 0);
+
+ // Should not crash with zero-size arrays
+ RTNAME(AssignSimple)(*dest, *source, __FILE__, __LINE__);
+
+ // Verify both still have 0 elements
+ EXPECT_EQ(dest->Elements(), 0);
+ EXPECT_EQ(source->Elements(), 0);
+
+ dest->Destroy();
+ source->Destroy();
+}
diff --git a/flang/include/flang/Runtime/assign.h b/flang/include/flang/Runtime/assign.h
index c145239c8c1ad..bf050ff890068 100644
--- a/flang/include/flang/Runtime/assign.h
+++ b/flang/include/flang/Runtime/assign.h
@@ -79,6 +79,10 @@ void RTDECL(AssignExplicitLengthCharacter)(Descriptor &to,
// This variant is assignments to whole polymorphic allocatables.
void RTDECL(AssignPolymorphic)(Descriptor &to, const Descriptor &from,
const char *sourceFile = nullptr, int sourceLine = 0);
+// Fast path for simple intrinsic type assignments (no derived types, no
+// finalization)
+void RTDECL(AssignSimple)(Descriptor &to, const Descriptor &from,
+ const char *sourceFile = nullptr, int sourceLine = 0);
} // extern "C"
} // namespace Fortran::runtime
#endif // FORTRAN_RUNTIME_ASSIGN_H_
More information about the flang-commits
mailing list