[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