[flang-commits] [flang] [llvm] [flang-rt] Add PDT LEN parameter support: Concrete type cache (PR #181008)

via flang-commits flang-commits at lists.llvm.org
Tue Feb 17 12:06:53 PST 2026


https://github.com/tmjbios updated https://github.com/llvm/llvm-project/pull/181008

>From fc7f5bdd2fc646c2891e12b80d16d58d1a94fb40 Mon Sep 17 00:00:00 2001
From: Ted Johnson <tedmjohnson at protonmail.com>
Date: Tue, 10 Feb 2026 18:33:26 -0600
Subject: [PATCH 1/4] [flang-rt] Add PDT LEN parameter support: Concrete type
 cache

Implement a runtime cache that lazily instantiates Concrete DerivedType
objects for Parameterized Derived Types whose LEN parameters are only
known at runtime. Each unique <generic-type, LEN-values> combination
produces a Concrete type with resolved component offsets and total size,
stored in a hash map for O(1) reuse.

Also adds a Component alignment field for GPU-correct layout computation,
and integrates concrete type resolution into the allocatable, pointer,
assign, and nested-component paths.

This does not address the compile-time updates needed for a full,
complete solution.

This commit passes check-flang, check-flang-rt, and llvm-test-suite.
---
 .../include/flang-rt/runtime/descriptor.h     |  20 +-
 flang-rt/include/flang-rt/runtime/memory.h    |   3 +-
 .../flang-rt/runtime/type-info-cache.h        |  50 +++
 flang-rt/include/flang-rt/runtime/type-info.h |  28 +-
 flang-rt/lib/runtime/CMakeLists.txt           |   1 +
 flang-rt/lib/runtime/allocatable.cpp          |  20 +
 flang-rt/lib/runtime/assign.cpp               |   5 +
 flang-rt/lib/runtime/pointer.cpp              |  20 +
 flang-rt/lib/runtime/type-info-cache.cpp      | 374 ++++++++++++++++
 flang-rt/lib/runtime/type-info.cpp            |  32 +-
 flang/docs/PDT_ConcreteTypes.md               | 418 ++++++++++++++++++
 flang/include/flang/ISO_Fortran_binding.h     |   5 +-
 .../include/flang/Runtime/descriptor-consts.h |   8 +-
 flang/lib/Evaluate/type.cpp                   |  12 +-
 flang/lib/Semantics/runtime-type-info.cpp     |   7 +
 flang/module/__fortran_type_info.f90          |  13 +-
 .../Lower/CUDA/cuda-allocatable-device.cuf    |   6 +-
 flang/test/Lower/volatile-openmp.f90          |   4 +-
 flang/test/Semantics/typeinfo01.f90           |  14 +-
 flang/test/Semantics/typeinfo08.f90           |   2 +-
 20 files changed, 1010 insertions(+), 32 deletions(-)
 create mode 100644 flang-rt/include/flang-rt/runtime/type-info-cache.h
 create mode 100644 flang-rt/lib/runtime/type-info-cache.cpp
 create mode 100644 flang/docs/PDT_ConcreteTypes.md

diff --git a/flang-rt/include/flang-rt/runtime/descriptor.h b/flang-rt/include/flang-rt/runtime/descriptor.h
index 40e30e3bf783f..da3b7705e3a40 100644
--- a/flang-rt/include/flang-rt/runtime/descriptor.h
+++ b/flang-rt/include/flang-rt/runtime/descriptor.h
@@ -92,6 +92,19 @@ class Dimension {
   ISO::CFI_dim_t raw_;
 };
 
+// Flexible Array hack for C++; PDT LEN type paramter storage.
+// The storage for additional elements must be allocated immediately
+// following this struct. NOTE: access via operator[] uses pointer arithmetic.
+template <typename T> struct FlexibleArray {
+  T len_ntry_;
+  RT_API_ATTRS T &operator[](int index) { return (&len_ntry_)[index]; }
+  RT_API_ATTRS const T &operator[](int index) const {
+    return (&len_ntry_)[index];
+  }
+  RT_API_ATTRS operator T *() { return &len_ntry_; }
+  RT_API_ATTRS operator const T *() const { return &len_ntry_; }
+};
+
 // The storage for this object follows the last used dim[] entry in a
 // Descriptor (CFI_cdesc_t) generic descriptor.  Space matters here, since
 // descriptors serve as POINTER and ALLOCATABLE components of derived type
@@ -138,11 +151,8 @@ class DescriptorAddendum {
 
 private:
   const typeInfo::DerivedType *derivedType_;
-  typeInfo::TypeParameterValue len_[1]; // must be the last component
-  // The LEN type parameter values can also include captured values of
-  // specification expressions that were used for bounds and for LEN type
-  // parameters of components.  The values have been truncated to the LEN
-  // type parameter's type, if shorter than 64 bits, then sign-extended.
+  FlexibleArray<typeInfo::TypeParameterValue> len_;
+  // len_ MUST be the last component of the class.
 };
 
 // A C++ view of a standard descriptor object.
diff --git a/flang-rt/include/flang-rt/runtime/memory.h b/flang-rt/include/flang-rt/runtime/memory.h
index 93b477afa9814..955bbcd71b3ac 100644
--- a/flang-rt/include/flang-rt/runtime/memory.h
+++ b/flang-rt/include/flang-rt/runtime/memory.h
@@ -14,8 +14,9 @@
 
 #include "flang/Common/api-attrs.h"
 #include <cassert>
-#include <memory>
+#include <cstddef> // std::nullptr_t
 #include <type_traits>
+#include <utility>
 
 namespace Fortran::runtime {
 
diff --git a/flang-rt/include/flang-rt/runtime/type-info-cache.h b/flang-rt/include/flang-rt/runtime/type-info-cache.h
new file mode 100644
index 0000000000000..dc4ae424ed672
--- /dev/null
+++ b/flang-rt/include/flang-rt/runtime/type-info-cache.h
@@ -0,0 +1,50 @@
+//===-- include/flang-rt/runtime/type-info-cache.h ----------------*- C++
+//-*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Cache for concrete PDT (Parameterized Derived Type) instantiations.
+//
+// For types with LEN parameters, layout depends on runtime values. This cache
+// stores resolved "concrete types" keyed by (generic_type, len_values...) so
+// that all instances with identical LEN values share the same type description.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef FLANG_RT_RUNTIME_TYPE_INFO_CACHE_H_
+#define FLANG_RT_RUNTIME_TYPE_INFO_CACHE_H_
+
+#include "flang-rt/runtime/descriptor.h"
+#include "flang-rt/runtime/type-info.h"
+
+namespace Fortran::runtime {
+class Terminator;
+} // namespace Fortran::runtime
+
+namespace Fortran::runtime::typeInfo {
+
+// Get the concrete type for a generic PDT instantiated with specific LEN
+// values.
+//
+// If the type has no LEN parameters, returns the generic type unchanged.
+// Otherwise, looks up or creates a concrete type with resolved offsets/sizes.
+//
+// Parameters:
+//   genericType - The uninstantiated (generic) DerivedType from compile time
+//   instance    - A descriptor whose addendum contains the actual LEN values
+//   terminator  - For error handling
+//
+// Returns:
+//   Pointer to concrete DerivedType (may be same as genericType if no LEN
+//   params)
+//
+RT_API_ATTRS const DerivedType *GetConcreteType(const DerivedType &genericType,
+    const Descriptor &instance, runtime::Terminator &terminator);
+
+} // namespace Fortran::runtime::typeInfo
+
+#endif // FLANG_RT_RUNTIME_TYPE_INFO_CACHE_H_
diff --git a/flang-rt/include/flang-rt/runtime/type-info.h b/flang-rt/include/flang-rt/runtime/type-info.h
index e112ee418dcf3..e097a9bf8ec63 100644
--- a/flang-rt/include/flang-rt/runtime/type-info.h
+++ b/flang-rt/include/flang-rt/runtime/type-info.h
@@ -68,18 +68,30 @@ class Component {
   RT_API_ATTRS const Descriptor &name() const { return name_.descriptor(); }
   RT_API_ATTRS Genre genre() const { return genre_; }
   RT_API_ATTRS MemorySpace memorySpace() const { return memorySpace_; }
+  RT_API_ATTRS std::size_t alignment() const {
+    return std::size_t{1} << alignment_; // alignment_ stores the log2,
+                                         // not the actual value.
+  }
   RT_API_ATTRS TypeCategory category() const {
     return static_cast<TypeCategory>(category_);
   }
   RT_API_ATTRS int kind() const { return kind_; }
   RT_API_ATTRS int rank() const { return rank_; }
   RT_API_ATTRS std::uint64_t offset() const { return offset_; }
+  RT_API_ATTRS Component &SetOffset(std::uint64_t offset) {
+    offset_ = offset;
+    return *this;
+  }
   RT_API_ATTRS const Value &characterLen() const { return characterLen_; }
   RT_API_ATTRS const DerivedType *derivedType() const {
     return category() == TypeCategory::Derived
         ? derivedType_.descriptor().OffsetElement<const DerivedType>()
         : nullptr;
   }
+  RT_API_ATTRS Component &SetDerivedType(const DerivedType *dt) {
+    derivedType_.descriptor().set_base_addr(const_cast<DerivedType *>(dt));
+    return *this;
+  }
   RT_API_ATTRS const Value *lenValue() const {
     return lenValue_.descriptor().OffsetElement<const Value>();
   }
@@ -114,7 +126,9 @@ class Component {
   std::uint8_t kind_{0};
   std::uint8_t rank_{0};
   MemorySpace memorySpace_{MemorySpace::Host}; // memory space of the component
-  [[maybe_unused]] std::uint8_t padding_[3]; // 3 bytes padding
+  std::uint8_t alignment_{
+      0}; // log-2 value; alignment in bytes = (1 << alignment_)
+  [[maybe_unused]] std::uint8_t padding_[2]; // 2 bytes padding
   std::uint64_t offset_{0};
   Value characterLen_; // for TypeCategory::Character
   StaticDescriptor<0, true> derivedType_; // TYPE(DERIVEDTYPE), POINTER
@@ -227,9 +241,17 @@ class DerivedType {
   }
   RT_API_ATTRS const Descriptor &name() const { return name_.descriptor(); }
   RT_API_ATTRS std::uint64_t sizeInBytes() const { return sizeInBytes_; }
+  RT_API_ATTRS DerivedType &SetSizeInBytes(std::uint64_t bytes) {
+    sizeInBytes_ = bytes;
+    return *this;
+  }
   RT_API_ATTRS const Descriptor &uninstantiated() const {
     return uninstantiated_.descriptor();
   }
+  RT_API_ATTRS DerivedType &SetUninstantiatedType(const DerivedType *dt) {
+    uninstantiated_.descriptor().set_base_addr(const_cast<DerivedType *>(dt));
+    return *this;
+  }
   RT_API_ATTRS const DerivedType *uninstantiatedType() const {
     return reinterpret_cast<const DerivedType *>(
         uninstantiated().raw().base_addr);
@@ -243,6 +265,10 @@ class DerivedType {
   RT_API_ATTRS const Descriptor &component() const {
     return component_.descriptor();
   }
+  RT_API_ATTRS DerivedType &SetComponentBaseAddr(void *p) {
+    component_.descriptor().set_base_addr(p);
+    return *this;
+  }
   RT_API_ATTRS const Descriptor &procPtr() const {
     return procPtr_.descriptor();
   }
diff --git a/flang-rt/lib/runtime/CMakeLists.txt b/flang-rt/lib/runtime/CMakeLists.txt
index 787d0dbbfb5ca..0030743ba3f23 100644
--- a/flang-rt/lib/runtime/CMakeLists.txt
+++ b/flang-rt/lib/runtime/CMakeLists.txt
@@ -67,6 +67,7 @@ set(supported_sources
   transformational.cpp
   type-code.cpp
   type-info.cpp
+  type-info-cache.cpp
   unit.cpp
   utf.cpp
   work-queue.cpp
diff --git a/flang-rt/lib/runtime/allocatable.cpp b/flang-rt/lib/runtime/allocatable.cpp
index 5b3db1e47238b..6a3e0a4947428 100644
--- a/flang-rt/lib/runtime/allocatable.cpp
+++ b/flang-rt/lib/runtime/allocatable.cpp
@@ -12,6 +12,7 @@
 #include "flang-rt/runtime/descriptor.h"
 #include "flang-rt/runtime/stat.h"
 #include "flang-rt/runtime/terminator.h"
+#include "flang-rt/runtime/type-info-cache.h"
 #include "flang-rt/runtime/type-info.h"
 #include "flang/Common/ISO_Fortran_binding_wrapper.h"
 #include "flang/Runtime/assign.h"
@@ -40,6 +41,15 @@ void RTDEF(AllocatableInitDerived)(Descriptor &descriptor,
   INTERNAL_CHECK(corank == 0);
   descriptor.Establish(
       derivedType, nullptr, rank, nullptr, CFI_attribute_allocatable);
+  if (DescriptorAddendum * addendum{descriptor.Addendum()}) {
+    if (derivedType.LenParameters() > 0) {
+      Terminator terminator{__FILE__, __LINE__};
+      const typeInfo::DerivedType *concrete{
+          typeInfo::GetConcreteType(derivedType, descriptor, terminator)};
+      addendum->set_derivedType(concrete);
+      descriptor.raw().elem_len = concrete->sizeInBytes();
+    }
+  }
 }
 
 void RTDEF(AllocatableInitIntrinsicForAllocate)(Descriptor &descriptor,
@@ -142,6 +152,16 @@ int RTDEF(AllocatableAllocate)(Descriptor &descriptor,
   } else if (descriptor.IsAllocated()) {
     return ReturnError(terminator, StatBaseNotNull, errMsg, hasStat);
   } else {
+    if (DescriptorAddendum * addendum{descriptor.Addendum()}) {
+      if (const auto *derived{addendum->derivedType()}) {
+        if (derived->LenParameters() > 0) {
+          const typeInfo::DerivedType *concrete{
+              typeInfo::GetConcreteType(*derived, descriptor, terminator)};
+          addendum->set_derivedType(concrete);
+          descriptor.raw().elem_len = concrete->sizeInBytes();
+        }
+      }
+    }
     int stat{ReturnError(
         terminator, descriptor.Allocate(asyncObject), errMsg, hasStat)};
     if (stat == StatOk) {
diff --git a/flang-rt/lib/runtime/assign.cpp b/flang-rt/lib/runtime/assign.cpp
index 0d0710382a055..249b5931b521a 100644
--- a/flang-rt/lib/runtime/assign.cpp
+++ b/flang-rt/lib/runtime/assign.cpp
@@ -13,6 +13,7 @@
 #include "flang-rt/runtime/stat.h"
 #include "flang-rt/runtime/terminator.h"
 #include "flang-rt/runtime/tools.h"
+#include "flang-rt/runtime/type-info-cache.h"
 #include "flang-rt/runtime/type-info.h"
 #include "flang-rt/runtime/work-queue.h"
 
@@ -111,6 +112,10 @@ static RT_API_ATTRS int AllocateAssignmentLHS(
   }
   to.raw().type = from.raw().type;
   if (derived) {
+    if (derived->LenParameters() > 0 && toAddendum) {
+      derived = typeInfo::GetConcreteType(*derived, to, terminator);
+      toAddendum->set_derivedType(derived);
+    }
     to.raw().elem_len = derived->sizeInBytes();
   } else if (!(flags & ExplicitLengthCharacterLHS)) {
     to.raw().elem_len = from.ElementBytes();
diff --git a/flang-rt/lib/runtime/pointer.cpp b/flang-rt/lib/runtime/pointer.cpp
index 0832b5656f1ab..48701ecbd8c90 100644
--- a/flang-rt/lib/runtime/pointer.cpp
+++ b/flang-rt/lib/runtime/pointer.cpp
@@ -14,6 +14,7 @@
 #include "flang-rt/runtime/stat.h"
 #include "flang-rt/runtime/terminator.h"
 #include "flang-rt/runtime/tools.h"
+#include "flang-rt/runtime/type-info-cache.h"
 #include "flang-rt/runtime/type-info.h"
 
 namespace Fortran::runtime {
@@ -39,6 +40,15 @@ void RTDEF(PointerNullifyDerived)(Descriptor &pointer,
     const typeInfo::DerivedType &derivedType, int rank, int corank) {
   INTERNAL_CHECK(corank == 0);
   pointer.Establish(derivedType, nullptr, rank, nullptr, CFI_attribute_pointer);
+  if (DescriptorAddendum * addendum{pointer.Addendum()}) {
+    if (derivedType.LenParameters() > 0) {
+      Terminator terminator{__FILE__, __LINE__};
+      const typeInfo::DerivedType *concrete{
+          typeInfo::GetConcreteType(derivedType, pointer, terminator)};
+      addendum->set_derivedType(concrete);
+      pointer.raw().elem_len = concrete->sizeInBytes();
+    }
+  }
 }
 
 void RTDEF(PointerSetBounds)(Descriptor &pointer, int zeroBasedDim,
@@ -163,6 +173,16 @@ int RTDEF(PointerAllocate)(Descriptor &pointer, bool hasStat,
   if (!pointer.IsPointer()) {
     return ReturnError(terminator, StatInvalidDescriptor, errMsg, hasStat);
   }
+  if (DescriptorAddendum * addendum{pointer.Addendum()}) {
+    if (const auto *derived{addendum->derivedType()}) {
+      if (derived->LenParameters() > 0) {
+        const typeInfo::DerivedType *concrete{
+            typeInfo::GetConcreteType(*derived, pointer, terminator)};
+        addendum->set_derivedType(concrete);
+        pointer.raw().elem_len = concrete->sizeInBytes();
+      }
+    }
+  }
   std::size_t elementBytes{pointer.ElementBytes()};
   if (static_cast<std::int64_t>(elementBytes) < 0) {
     // F'2023 7.4.4.2 p5: "If the character length parameter value evaluates
diff --git a/flang-rt/lib/runtime/type-info-cache.cpp b/flang-rt/lib/runtime/type-info-cache.cpp
new file mode 100644
index 0000000000000..2696b348f1261
--- /dev/null
+++ b/flang-rt/lib/runtime/type-info-cache.cpp
@@ -0,0 +1,374 @@
+//===-- lib/runtime/type-info-cache.cpp -------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "flang-rt/runtime/type-info-cache.h"
+#include "flang-rt/runtime/memory.h"
+#include "flang-rt/runtime/terminator.h"
+
+#ifndef FLANG_RT_PDT_CACHE_MAX_LOAD_FACTOR
+#define FLANG_RT_PDT_CACHE_MAX_LOAD_FACTOR 2
+#endif
+
+#ifndef FLANG_RT_PDT_CACHE_INITIAL_BUCKET_CNT
+#define FLANG_RT_PDT_CACHE_INITIAL_BUCKET_CNT 31
+#endif
+
+namespace Fortran::runtime::typeInfo {
+
+#ifdef RT_DEVICE_COMPILATION
+
+// Device stub: PDT LEN parameter instantiation not supported on GPU
+RT_API_ATTRS const DerivedType *GetConcreteType(const DerivedType &genericType,
+    const Descriptor &instance, runtime::Terminator &terminator) {
+  std::size_t numLenParams = genericType.LenParameters();
+  // Types without LEN params or already-concrete types can pass through
+  if ((numLenParams == 0) ||
+      (genericType.uninstantiatedType() != &genericType)) {
+    return &genericType;
+  }
+  // Cannot instantiate PDT with LEN parameters on device
+  terminator.Crash(
+      "PDT LEN parameter instantiation not supported in device code");
+  return nullptr;
+}
+
+#else // !RT_DEVICE_COMPILATION
+
+#include <cstdlib>
+#include <cstring>
+#include <functional>
+
+// using Fortran::runtime::FreeMemory;
+using Fortran::runtime::New;
+using Fortran::runtime::OwningPtr;
+using Fortran::runtime::Terminator;
+
+namespace {
+
+// Hash table entry for concrete type cache
+struct CacheEntry {
+  std::uint64_t hash;
+  DerivedType *concreteType;
+  OwningPtr<CacheEntry> next{nullptr};
+
+  explicit CacheEntry(std::uint64_t h, DerivedType *type)
+      : hash{h}, concreteType{type} {}
+};
+
+// Dynamically resizing hash table using malloc/free (no C++ allocators)
+class ConcreteTypeCache {
+public:
+  ConcreteTypeCache() {
+    numBuckets_ = initialBuckets_;
+    buckets_ = static_cast<CacheEntry **>(
+        std::calloc(numBuckets_, sizeof(CacheEntry *)));
+  }
+
+  ~ConcreteTypeCache() {
+    if (buckets_) {
+      // Free all chains
+      for (std::size_t i = 0; i < numBuckets_; ++i) {
+        FreeBucketChain(buckets_[i]);
+      }
+      std::free(buckets_);
+    }
+  }
+
+  DerivedType *Find(std::uint64_t hash) {
+    if (!buckets_) {
+      return nullptr;
+    }
+    std::size_t index = hash % numBuckets_;
+    for (CacheEntry *entry = buckets_[index]; entry;
+        entry = entry->next.get()) {
+      if (entry->hash == hash) {
+        return entry->concreteType;
+      }
+    }
+    return nullptr;
+  }
+
+  void Insert(
+      std::uint64_t hash, DerivedType *type, const Terminator &terminator) {
+    if (!buckets_) {
+      return;
+    }
+
+    // Check if we need to resize
+    if (numEntries_ >= numBuckets_ * maxLoadFactor_) {
+      Resize(terminator);
+    }
+
+    std::size_t index = hash % numBuckets_;
+    CacheEntry *newEntry = New<CacheEntry>{terminator}(hash, type).release();
+    newEntry->next.reset(buckets_[index]);
+    buckets_[index] = newEntry;
+    ++numEntries_;
+  }
+
+private:
+  static void FreeBucketChain(CacheEntry *head) {
+    while (head) {
+      CacheEntry *next = head->next.release();
+      Fortran::runtime::FreeMemory(head);
+      head = next;
+    }
+  }
+
+  void Resize(const Terminator &terminator) {
+    std::size_t oldNumBuckets = numBuckets_;
+    CacheEntry **oldBuckets = buckets_;
+
+    // Double the bucket count
+    numBuckets_ = oldNumBuckets * 2;
+    buckets_ = static_cast<CacheEntry **>(
+        std::calloc(numBuckets_, sizeof(CacheEntry *)));
+
+    if (!buckets_) {
+      // Allocation failed - restore old state
+      buckets_ = oldBuckets;
+      numBuckets_ = oldNumBuckets;
+      return;
+    }
+
+    // Rehash all entries from old buckets
+    for (std::size_t i = 0; i < oldNumBuckets; ++i) {
+      CacheEntry *entry = oldBuckets[i];
+      while (entry) {
+        CacheEntry *next = entry->next.release();
+
+        // Insert into new bucket
+        std::size_t newIndex = entry->hash % numBuckets_;
+        entry->next.reset(buckets_[newIndex]);
+        buckets_[newIndex] = entry;
+
+        entry = next;
+      }
+    }
+
+    // Clean up old bucket array (entries already moved)
+    std::free(oldBuckets);
+  }
+
+  static constexpr std::size_t initialBuckets_{
+      FLANG_RT_PDT_CACHE_INITIAL_BUCKET_CNT};
+  static constexpr std::size_t maxLoadFactor_{
+      FLANG_RT_PDT_CACHE_MAX_LOAD_FACTOR};
+
+  CacheEntry **buckets_{nullptr};
+  std::size_t numBuckets_{0};
+  std::size_t numEntries_{0};
+};
+
+static ConcreteTypeCache concreteTypeCache;
+
+// Compute hash from generic type pointer and LEN parameter values
+// using a hash combining formula based on Boost's hash_combine.
+static std::uint64_t ComputeConcreteTypeHash(const DerivedType &genericType,
+    const DescriptorAddendum &addendum, std::size_t numLenParams) {
+  std::uint64_t hash = reinterpret_cast<std::uintptr_t>(&genericType);
+  for (std::size_t i = 0; i < numLenParams; ++i) {
+    TypeParameterValue v = addendum.LenParameterValue(i);
+    hash ^= std::hash<TypeParameterValue>{}(v) + 0x9e3779b9 + (hash << 6) +
+        (hash >> 2);
+  }
+  return hash;
+}
+
+} // anonymous namespace
+
+// Compute allocation size for a concrete type: DerivedType + Component array
+static std::size_t ComputeConcreteTypeAllocationSize(
+    const DerivedType &generic) {
+  std::size_t numComponents = generic.component().Elements();
+  // Ensure Component array is properly aligned after DerivedType
+  static_assert(sizeof(DerivedType) % alignof(Component) == 0,
+      "DerivedType size must be aligned for trailing Component array");
+  return sizeof(DerivedType) + numComponents * sizeof(Component);
+}
+
+// Copy generic type to concrete, setting up the Component array pointer
+static void CopyGenericToConcreteType(DerivedType *concrete,
+    const DerivedType &generic, Component *concreteComponents,
+    std::size_t numComponents) {
+  // NOTE: DerivedType has a user-declared destructor, so it is not trivially
+  // copyable. We still rely on this shallow copy because its members are
+  // descriptor wrappers/pointers to shared immutable metadata (bindings, name,
+  // kindParameter, lenParameterKind, procPtr, special).
+  std::memcpy(static_cast<void *>(concrete), &generic, sizeof(DerivedType));
+
+  // Copy the Component array
+  const Descriptor &genericCompDesc = generic.component();
+  const Component *genericComponents =
+      genericCompDesc.OffsetElement<const Component>();
+  std::memcpy(
+      concreteComponents, genericComponents, numComponents * sizeof(Component));
+
+  concrete->SetComponentBaseAddr(concreteComponents);
+  concrete->SetUninstantiatedType(&generic);
+}
+
+// Get alignment requirement for a component (stored by compiler)
+static std::size_t GetComponentAlignment(const Component &comp) {
+  return comp.alignment();
+}
+
+// Resolve LEN-dependent offsets in the concrete type's Component array
+static std::size_t ResolveComponentOffsets(Component *components,
+    std::size_t numComponents, const Descriptor &instance,
+    runtime::Terminator &terminator) {
+  std::size_t currentOffset = 0;
+  std::size_t maxAlignment = 1;
+
+  for (std::size_t j = 0; j < numComponents; ++j) {
+    Component &comp = components[j];
+
+    // Use existing method to compute element byte size
+    std::size_t elementBytes = comp.GetElementByteSize(instance);
+    std::size_t alignment = GetComponentAlignment(comp);
+
+    // Compute element count for array components
+    std::size_t numElements = 1;
+    if (int rank = comp.rank(); rank > 0) {
+      if (const Value *boundValues = comp.bounds()) {
+        for (int dim = 0; dim < rank; ++dim) {
+          auto lb = boundValues[2 * dim].GetValue(&instance);
+          auto ub = boundValues[2 * dim + 1].GetValue(&instance);
+          if (lb.has_value() && ub.has_value() && *ub >= *lb) {
+            numElements *= (*ub - *lb + 1);
+          } else {
+            numElements = 0;
+            break;
+          }
+        }
+      }
+    }
+
+    // Compute component size based on genre
+    std::size_t componentSize = 0;
+
+    if (comp.genre() != Component::Genre::Data) {
+      // Non-Data genres (Allocatable, Pointer, Automatic): store a Descriptor
+      const DerivedType *derivedComp = comp.derivedType();
+      componentSize = Descriptor::SizeInBytes(
+          comp.rank(), true, derivedComp ? derivedComp->LenParameters() : 0);
+      alignment = alignof(Descriptor);
+    } else if (const DerivedType *nestedType{comp.derivedType()};
+        comp.category() == TypeCategory::Derived && nestedType &&
+        nestedType->LenParameters() > 0) {
+      // Nested PDT with LEN params: resolve concrete type depth-first
+      const Value *lenValues = comp.lenValue();
+      RUNTIME_CHECK(terminator, lenValues != nullptr);
+
+      OwningPtr<Descriptor> tempDesc{Descriptor::Create(
+          *nestedType, nullptr, 0, nullptr, CFI_attribute_other)};
+      DescriptorAddendum *tempAddendum = tempDesc->Addendum();
+      RUNTIME_CHECK(terminator, tempAddendum != nullptr);
+
+      std::size_t nestedLenParams = nestedType->LenParameters();
+      for (std::size_t i = 0; i < nestedLenParams; ++i) {
+        auto value = lenValues[i].GetValue(&instance);
+        RUNTIME_CHECK(terminator, value.has_value());
+        tempAddendum->SetLenParameterValue(i, *value);
+      }
+
+      const DerivedType *nestedConcrete =
+          GetConcreteType(*nestedType, *tempDesc, terminator);
+      elementBytes = nestedConcrete->sizeInBytes();
+
+      comp.SetDerivedType(nestedConcrete);
+      componentSize = elementBytes * numElements;
+    } else {
+      // Data genre: intrinsic, non-PDT derived, or KIND-only PDT
+      componentSize = elementBytes * numElements;
+    }
+
+    // Apply alignment; round up to the next multiple, if needed
+    if (alignment > 1) {
+      currentOffset = (currentOffset + alignment - 1) & ~(alignment - 1);
+    }
+
+    comp.SetOffset(currentOffset);
+
+    currentOffset += componentSize;
+    if (alignment > maxAlignment) {
+      maxAlignment = alignment;
+    }
+  }
+
+  // Final structure alignment
+  if (maxAlignment > 1) {
+    currentOffset = (currentOffset + maxAlignment - 1) & ~(maxAlignment - 1);
+  }
+
+  return currentOffset; // This becomes sizeInBytes_
+}
+
+// Create a new concrete type from a generic type and specific LEN values
+static DerivedType *CreateConcreteType(const DerivedType &generic,
+    const Descriptor &instance, runtime::Terminator &terminator) {
+  std::size_t numComponents = generic.component().Elements();
+  std::size_t allocSize = ComputeConcreteTypeAllocationSize(generic);
+
+  void *memory = std::calloc(allocSize, 1);
+  RUNTIME_CHECK(terminator, memory != nullptr);
+
+  DerivedType *concrete = static_cast<DerivedType *>(memory);
+  Component *components = reinterpret_cast<Component *>(
+      static_cast<char *>(memory) + sizeof(DerivedType));
+
+  CopyGenericToConcreteType(concrete, generic, components, numComponents);
+
+  std::size_t sizeInBytes =
+      ResolveComponentOffsets(components, numComponents, instance, terminator);
+
+  concrete->SetSizeInBytes(sizeInBytes);
+
+  return concrete;
+}
+
+RT_API_ATTRS const DerivedType *GetConcreteType(const DerivedType &genericType,
+    const Descriptor &instance, runtime::Terminator &terminator) {
+  std::size_t numLenParams = genericType.LenParameters();
+  // Fast path: No LEN params, or we already have a concrete type.
+  // This occurs when the descriptor's derivedType_ was previously set
+  // to a concrete type, and a subsequent operation
+  // (e.g., AllocatableAllocate) calls GetConcreteType again with that
+  // already-resolved type. Concrete types have their uninstantiatedType_
+  // pointing to the original generic, not to themselves.
+  if ((numLenParams == 0) ||
+      (genericType.uninstantiatedType() != &genericType)) {
+    return &genericType; // Already concrete, return as-is
+  }
+
+  // Check that instance has addendum with LEN values
+  const DescriptorAddendum *addendum = instance.Addendum();
+  RUNTIME_CHECK(terminator, addendum != nullptr);
+
+  // Compute hash directly from LEN values (no heap allocation)
+  std::uint64_t hash =
+      ComputeConcreteTypeHash(genericType, *addendum, numLenParams);
+
+  // Check cache
+  DerivedType *cached = concreteTypeCache.Find(hash);
+  if (cached) {
+    return cached; // Cache hit
+  }
+
+  // Cache miss: create new concrete type
+  DerivedType *concrete = CreateConcreteType(genericType, instance, terminator);
+
+  // Insert into cache
+  concreteTypeCache.Insert(hash, concrete, terminator);
+
+  return concrete;
+}
+
+#endif // RT_DEVICE_COMPILATION
+
+} // namespace Fortran::runtime::typeInfo
diff --git a/flang-rt/lib/runtime/type-info.cpp b/flang-rt/lib/runtime/type-info.cpp
index cb8e894bd3922..5c3232eb0ad64 100644
--- a/flang-rt/lib/runtime/type-info.cpp
+++ b/flang-rt/lib/runtime/type-info.cpp
@@ -9,6 +9,7 @@
 #include "flang-rt/runtime/type-info.h"
 #include "flang-rt/runtime/terminator.h"
 #include "flang-rt/runtime/tools.h"
+#include "flang-rt/runtime/type-info-cache.h"
 #include <cstdio>
 
 namespace Fortran::runtime::typeInfo {
@@ -32,6 +33,34 @@ RT_API_ATTRS common::optional<TypeParameterValue> Value::GetValue(
   }
 }
 
+static const DerivedType *ResolveDerivedTypeForComponent(const Component &comp,
+    const Descriptor &container, Terminator &terminator) {
+  if (comp.category() != TypeCategory::Derived) {
+    return nullptr;
+  }
+  const DerivedType *generic{comp.derivedType()};
+  if (!generic || generic->LenParameters() == 0) {
+    return generic;
+  }
+
+  const Value *lenValues{comp.lenValue()};
+  RUNTIME_CHECK(terminator, lenValues != nullptr);
+
+  OwningPtr<Descriptor> tempDesc{
+      Descriptor::Create(*generic, nullptr, 0, nullptr, CFI_attribute_other)};
+  DescriptorAddendum *tempAddendum{tempDesc->Addendum()};
+  RUNTIME_CHECK(terminator, tempAddendum != nullptr);
+
+  std::size_t lenParams{generic->LenParameters()};
+  for (std::size_t i{0}; i < lenParams; ++i) {
+    auto value{lenValues[i].GetValue(&container)};
+    RUNTIME_CHECK(terminator, value.has_value());
+    tempAddendum->SetLenParameterValue(i, *value);
+  }
+
+  return GetConcreteType(*generic, *tempDesc, terminator);
+}
+
 RT_API_ATTRS std::size_t Component::GetElementByteSize(
     const Descriptor &instance) const {
   switch (category()) {
@@ -118,7 +147,8 @@ RT_API_ATTRS void Component::EstablishDescriptor(Descriptor &descriptor,
     descriptor.Establish(kind_, lengthInChars, nullptr, rank_, nullptr,
         attribute, false, allocatorIdx);
   } else if (cat == TypeCategory::Derived) {
-    if (const DerivedType * type{derivedType()}) {
+    if (const DerivedType *type{
+            ResolveDerivedTypeForComponent(*this, container, terminator)}) {
       descriptor.Establish(
           *type, nullptr, rank_, nullptr, attribute, allocatorIdx);
     } else { // unlimited polymorphic
diff --git a/flang/docs/PDT_ConcreteTypes.md b/flang/docs/PDT_ConcreteTypes.md
new file mode 100644
index 0000000000000..0d49f5d5e8890
--- /dev/null
+++ b/flang/docs/PDT_ConcreteTypes.md
@@ -0,0 +1,418 @@
+# PDT Implementation: Concrete Type Instantiation
+
+This document describes the design for implementing Parameterized Derived Types
+(PDTs) with LEN type parameters in Flang using a "concrete type" approach. This
+strategy resolves LEN-dependent type layouts at runtime and caches the results,
+combining the flexibility of runtime evaluation with the performance of
+pre-computed offsets.
+
+
+## Problem Statement
+
+For PDTs with LEN parameters, component offsets depend on runtime values:
+
+```fortran
+! Simple PDT with LEN type parameter
+type :: pdt(N)
+  integer, len   :: N
+  character(N*2) :: str        ! size = 2*N bytes
+  integer        :: after_str  ! offset within PDT depends on runtime val of N
+end type
+
+! More complex PDT with both LEN and KIND type parameters
+type :: pdt2(M,N,K)
+  integer, len  :: M,N
+  integer, kind :: K
+  real          :: R1(M)  ! Uses runtime value of M
+  real(kind=K)  :: R2     ! Offset depends on runtime M * size of default REAL
+  integer       :: VAL(N) ! Offset depends on previous members, and must account
+                          ! for the compile-time KIND value as well. 
+end type
+
+! Nested, with LEN type parameter
+type :: nestedType(A, B)
+  integer, len  :: A,B
+  type(pdt(A)) :: X ! Nested, pass "A" to component
+  type(pdt(B)) :: Y ! Nested, pass "B" to component
+end type
+```
+
+The `after_str` component's offset cannot be determined at compile time because
+`N` is a LEN parameter whose value is only known on instantiation.
+
+
+## Solution: Concrete Type Instantiation
+
+The concrete type approach creates resolved type descriptions lazily at runtime
+and caches them. All instances with identical LEN values share the same
+concrete type, providing O(1) component access after initial instantiation.
+
+For any specific set of LEN values, the type layout is fully determined. All
+instances of `pdt(10)` have identical component offsets. Rather than
+recomputing offsets on each access, we resolve them once and cache the result.
+
+
+### Architecture Overview
+
+```
+                    +---------------------------+
+                    | DerivedType "pdt"         |
+                    | (generic/uninstantiated)  |
+                    | offset_: 0 (placeholder)  |<------------------+
+                    | numLenParams: 1           |                   |
+                    | sizeInBytes_: 0           |                   |
+                    | uninstantiated_: self     |<----------------+ |
+                    +---------------------------+                 | |
+            .                                                     | |
+            .                                                     | |
++------------------------+     +-------------------------------+  | |
+| Descriptor             |     | Concrete DerivedType          |  | |
+| type(pdt(N=10)) :: A   |     | Hash: 0xABC123                |  | |
+| len_ = {10}            |     | 0x7fff8a003000 (heap)         |  | |
+| derivedType_:----------|---->| offset_: {0, 4, 24}           |  | |
++------------------------+     | sizeInBytes_: 28              |  | |
+            .                  | uninstantiated_: -------------|--+ |
+            .                  | numLenParams: 1               |    |
+            .                  +-------------------------------+    |
++------------------------+               ^                          |
+| Descriptor             |               |                          |
+| type(pdt(N=10)) :: B   |               |                          |
+| len_[] = {10}          |               |                          |
+| derivedType_: ---------|---------------+                          |
++------------------------+                                          |
+            .                                                       |
++------------------------+     +----------------------------------+ |
+| Descriptor             |     | Concrete DerivedType             | |
+| type(pdt(N=20)) :: C   |     | 0x7fff8a004000 (heap)            | |
+| len_ = {20}            |     | offset_: {0, 4,  44}             | |
+| derivedType_:----------|---->| sizeInBytes_: 48                 | |
++------------------------+     | uninstantiated_: ----------------|-+
+            .                  | numLenParams: 1                  |
+            .                  +----------------------------------+
+```
+
+## Data Structures
+
+### Value Class
+
+The existing `Value` class already supports LEN-dependent expressions:
+
+```cpp
+class Value {
+public:
+  enum class Genre : std::uint8_t {
+    Deferred = 1,      // runtime-determined (ALLOCATABLE, POINTER)
+    Explicit = 2,      // constant value
+    LenParameter = 3,  // references LEN parameter by index
+  };
+  ...
+  RT_API_ATTRS common::optional<TypeParameterValue>
+    GetValue(const Descriptor *) const;
+  ...
+private:
+  ...
+  Genre genre_{Genre::Explicit};
+  TypeParameterValue value_{0};
+};
+```
+
+When `genre_ == LenParameter`, `value_` is an index into the descriptor's
+`len_` Flexible array. The `GetValue()` method resolves this at runtime.
+
+### Component Class
+
+Each component of a derived type is described by a `Component`:
+
+```cpp
+class Component {
+  ...
+  StaticDescriptor<0> name_;
+  Genre genre_;                    // Data, Pointer, Allocatable, Automatic
+  std::uint8_t category_;          // TypeCategory
+  std::uint8_t kind_;
+  std::uint8_t rank_;
+  MemorySpace memorySpace_;
+  std::uint8_t alignment_;         // new field; log2 of targ-specific alignment
+  std::uint8_t padding_[2];
+  std::uint64_t offset_;           // byte offset within instance
+  Value characterLen_;             // for CHARACTER components
+  StaticDescriptor<0> derivedType_;
+  StaticDescriptor<1> lenValue_;   // LEN values for nested PDTs
+  StaticDescriptor<2> bounds_;     // array bounds (can be LEN-dependent)
+  const char *initialization_;
+};
+```
+
+The `characterLen_` and `bounds_` fields use `Value` to express LEN-dependent
+sizes. The `alignment_` field stores `log2(alignment)` for correct layout
+computation on heterogeneous systems (CPU/GPU). We add a public method for
+changing `offset_` so we can create the ConcreteType members with proper
+offsets.
+
+### DescriptorAddendum
+
+Per-instance LEN values are stored in the descriptor's addendum:
+
+```cpp
+class DescriptorAddendum {
+  ...
+  const typeInfo::DerivedType *derivedType_;
+  FlexibleArray<typeInfo::TypeParameterValue> len_; // must be last component
+};
+```
+
+The `len_` field is changed from a fixed-size `TypeParameterValue len_[1]`
+array to `FlexibleArray<TypeParameterValue>`. This template wraps a single
+inline element `len_ntry_` and provides `operator[]` via pointer arithmetic,
+emulating a C flexible array member. Note that this is a different
+`FlexibleArray` from the one in `ISO_Fortran_binding.h` used for `dim`;
+that version inherits from `T` and indexes via `this`-pointer arithmetic. The
+`DescriptorAddendum::SizeInBytes()` calculation accounts for the one
+inline element: additional LEN parameters beyond the first require trailing
+storage allocated immediately after the struct.
+
+
+## KIND Type Parameter Instantiation
+
+Flang already uses an instantiation pattern for KIND type parameters that serves
+as our initial model for LEN parameter handling.
+
+For KIND parameters, the compiler generates a separate `DerivedType` at compile
+time for each unique KIND instantiation. Each instantiated type points back to
+the original uninstantiated type via `uninstantiated_`:
+
+```
++--------------------+                  +---------------------------+
+| Descriptor         |                  | DerivedType "pdt"         |
+| type(pdt(4)) :: A  |                  | (generic/uninstantiated)  |
+| derivedType_: -----|-+                | uninstantiated_: self     |
++--------------------+ |                +---------------------------+
+                       |                             ^           ^
++-------------------+  |                             |           |
+| Descriptor        |  |   +----------------------+  |           |
+| type(pdt(4)) :: B |  +-> | DerivedType "pdt.k4" |  |           |
+| derivedType_: ----|----> | 0x7fff8a001000 (heap)|  |           |
++-------------------+      | uninstantiated_:-----|--+           |
+                           +----------------------+              |
+                                                                 |
++-------------------+                  +----------------------+  |
+| Descriptor        |                  | DerivedType "pdt.k8" |  |
+| type(pdt(8)) :: C |                  | 0x7fff8a002000 (heap)|  |
+| derivedType_: ----|----------------->| uninstantiated_:-----|--+
++-------------------+                  +----------------------+
+```
+
+Descriptors A and B both store the same pointer (`0x7fff8a001000`) in
+their `derivedType_` field, referencing a single shared generic `DerivedType`
+structure.
+
+The `SameTypeAs` intrinsic in `derived-api` uses this pattern: if direct pointer comparison fails, it compares `uninstantiatedType()` pointers. This allows type matching across module boundaries.
+
+For LEN parameters, we apply the same pattern but create concrete types at
+runtime:
+
+- Concrete type's `uninstantiated_` points to the generic type
+- Cache key: `(genericType, len_values...)` for uniqueing the DerivedType
+- Use the existing `SAME_TYPE_AS` logic
+
+
+## Runtime Instantiation Algorithm
+
+### GetConcreteType Entry Point
+
+The runtime provides `GetConcreteType(genericType, instance, terminator)`:
+
+```cpp
+const DerivedType *
+GetConcreteType(const DerivedType &genericType,
+                const Descriptor  &instance,
+                Terminator &terminator);
+```
+
+1. If `genericType.LenParameters() == 0`, return `&genericType` (no LEN params)
+2. If `genericType.uninstantiatedType() != &genericType`, return `&genericType`
+   (already a concrete type from a previous call)
+3. Compute hash from `(genericType*, len_values...)`
+4. Check cache; if found, return cached concrete type
+5. Create new concrete type with resolved offsets
+6. Insert into cache; return pointer
+
+Step 2 handles the case where a descriptor's `derivedType_` was previously set
+to a concrete type, and a subsequent operation (e.g., `AllocatableAllocate`)
+passes that already-resolved type back to `GetConcreteType`. Concrete types
+have `uninstantiatedType_` pointing to the original generic; generic types
+point to themselves.
+
+
+### Concrete Type Creation
+
+When creating a new concrete type:
+
+1. Allocate `sizeof(DerivedType) + numComponents * sizeof(Component)`
+2. `memcpy` the generic type (Assumption: generic uses immutable metadata)
+3. Duplicate the components of the `Component[]` array
+4. Patch the pointer members of `DerivedType`:
+   - Set `component_` descriptor to point to new Component array
+   - Set `uninstantiated_` to point back to generic type
+5. For each component, resolve/compute actual offset based on
+   alignment and sizes (which themselves may depend on the LEN values)
+6. Set sizeInBytes to the final padded size of the resolved type
+
+
+### Offset Resolution
+
+Component offsets are computed sequentially, respecting alignment. Each
+component's size is determined by its genre:
+
+- **Non-Data genres** (Allocatable, Pointer, Automatic): the component stores
+  a Descriptor, so its size is `Descriptor::SizeInBytes(rank, ...)` and
+  alignment is `alignof(Descriptor)`.
+- **Nested PDT with LEN parameters** (Data genre, Derived category):
+  `GetConcreteType` is called recursively, depth-first, to resolve the inner
+  type. The resolved `sizeInBytes` is then used as the element size,
+  and the component's `derivedType_` pointer is updated to the concrete type.
+- **All other Data components** (intrinsic types, non-PDT derived, KIND-only
+  PDT): element byte size times element count.
+
+For array components, the element count is computed from the bounds (which
+may themselves be LEN-dependent `Value` expressions resolved against the
+parent descriptor).
+
+```cpp
+// Simplified view of ResolveComponentOffsets
+std::size_t currentOffset = 0;
+for (Component &comp : components) {
+    std::size_t alignment = comp.alignment();
+    currentOffset = alignTo(currentOffset, alignment);
+    comp.SetOffset(currentOffset);
+    currentOffset += componentSize;  // determined by genre (see above)
+}
+sizeInBytes = alignTo(currentOffset, maxAlignment);
+```
+
+### Cache Design
+
+The cache uses a custom dynamically-resizing hash table (`ConcreteTypeCache`)
+that relies only on C-style memory management (`malloc`, `calloc`, `free`).
+The hash value is used directly as the lookup key to avoid heap allocation
+on every query:
+
+```cpp
+// Hash combining formula based on Boost's hash_combine.
+std::uint64_t
+ComputeConcreteTypeHash(const DerivedType &genericType,
+                        const DescriptorAddendum &addendum,
+                        std::size_t numLenParams)
+{
+  std::uint64_t hash = reinterpret_cast<std::uintptr_t>(&genericType);
+  for (std::size_t i = 0; i < numLenParams; ++i) {
+    TypeParameterValue v = addendum.LenParameterValue(i);
+    hash ^= std::hash<TypeParameterValue>{}(v) +
+      0x9e3779b9 + (hash << 6) + (hash >> 2);
+  }
+  return hash;
+}
+```
+
+The hash table starts with 31 buckets (tunable at build time using
+-DFLANG_RT_PDT_CACHE_INITIAL_BUCKET_CNT), each holding a linked list of
+`CacheEntry` nodes, and the bucket count doubles when the average load reaches
+2 entries per bucket (again, tunable at build time using the define
+-DFLANG_RT_PDT_CACHE_MAX_LOAD_FACTOR). Bucket arrays are allocated with
+`std::calloc` while linked-list entries (`CacheEntry` nodes) use the runtime's
+`New<T>` / `FreeMemory` allocator. The concrete `DerivedType` objects themselves
+are allocated with `std::calloc`, not `New<T>`.
+
+Hash collisions are theoretically possible but extremely unlikely given a
+64-bit hash space. Note that `Find()` compares only hash values and does not
+verify the full key (`genericType` pointer + LEN values) after a match. A
+collision between two distinct `(genericType, len_values)` combinations would
+therefore silently return the wrong concrete type -- a correctness bug, not
+merely a performance issue. If collision resistance becomes a concern, a
+full-key equality check should be added to `Find()`.
+
+Concrete types are never freed (like generic types). The runtime currently
+assumes single-threaded allocation; if concurrent PDT allocation becomes a
+requirement, the existing `Lock` class from `lock.h` can be used.
+
+
+### Device Compilation
+
+GPU device code cannot use the host-side cache (which relies on `malloc` and
+dynamic data structures). When `RT_DEVICE_COMPILATION` is defined,
+`GetConcreteType` provides a pre-implementation stub that passes through types
+without LEN parameters or already-concrete types, and crashes for actual LEN
+instantiation requests:
+
+```cpp
+#ifdef RT_DEVICE_COMPILATION
+RT_API_ATTRS const DerivedType *GetConcreteType(...) {
+  if ((numLenParams == 0) || (genericType.uninstantiatedType() != &genericType)) {
+    return &genericType;
+  }
+  terminator.Crash("PDT LEN param instantiation not supported in device code");
+}
+#endif
+```
+
+## Lowering Strategy
+
+All LEN parameter expressions--even constant literals--are evaluated at runtime
+and stored in the descriptor's `len_` Flexible array. This uniform approach
+simplifies lowering by avoiding special-case code paths.
+
+```fortran
+type(pdt(4, n+1, m*2)) :: x
+```
+
+Lowers to:
+
+```mlir
+%len0 = arith.constant 4
+%len1 = arith.addi %n, %c1
+%len2 = arith.muli %m, %c2
+
+call @AllocatableSetDerivedLength(%desc, 0, %len0)
+call @AllocatableSetDerivedLength(%desc, 1, %len1)
+call @AllocatableSetDerivedLength(%desc, 2, %len2)
+call @AllocatableAllocate(%desc)  // internally calls GetConcreteType
+```
+
+LEN parameters do not affect type identity.
+`SAME_TYPE_AS(pdt(4), pdt(2+2))` returns `.TRUE.` because both evaluate to the same LEN value and thus the same member offsets. The cache correctly de-duplicates based on the resolved values.
+
+## Integration Points
+
+`GetConcreteType` is called in the following runtime paths:
+
+| File | Function | Purpose |
+|------|----------|--------|
+| `allocatable.cpp` | `AllocatableInitDerived` | Sets `derivedType_` to concrete on init |
+| `allocatable.cpp` | `AllocatableAllocate` | Resolves before allocation |
+| `pointer.cpp` | `PointerNullifyDerived` | Sets `derivedType_` to concrete on nullify |
+| `pointer.cpp` | `PointerAllocate` | Resolves before allocation |
+| `assign.cpp` | `AllocateAssignmentLHS` | Resolves for LHS reallocation |
+| `type-info.cpp` | `ResolveDerivedTypeForComponent` | Resolves nested PDT components within `EstablishDescriptor` |
+
+## Comparison of Approaches
+
+| Aspect | Outlined | Inlined | Concrete Types |
+|--------|----------|---------|----------------|
+| Compile-time work | High | Low | Low |
+| Runtime work | None | Per-access | One-time |
+| Runtime LEN values | Not possible | Supported | Supported |
+| Access performance | Fast | Slower | Fast |
+| Memory overhead | Per-instantiation | Per-component | Cache only |
+
+## Remaining Work
+
+The runtime implementation is complete.
+The following compiler-side work remains:
+
+1. For nested PDT components, verify the compiler emits `lenValue_` arrays
+   that map parent LEN parameters to child LEN parameters.
+
+2. Verify lowering correctly populates `DescriptorAddendum` LEN values via
+   `AllocatableSetDerivedLength` before calling allocation APIs.
+
+3. End-to-end testing with Fortran programs exercising PDTs with LEN parameters.
diff --git a/flang/include/flang/ISO_Fortran_binding.h b/flang/include/flang/ISO_Fortran_binding.h
index f5b8d0d2ea610..d5ef753927db4 100644
--- a/flang/include/flang/ISO_Fortran_binding.h
+++ b/flang/include/flang/ISO_Fortran_binding.h
@@ -153,8 +153,9 @@ extern "C++" template <typename T> struct FlexibleArray : T {
   CFI_rank_t rank; /* [0 .. CFI_MAX_RANK] */ \
   CFI_type_t type; \
   CFI_attribute_t attribute; \
-  /* This encodes both the presence of the f18Addendum and the index of the \
-   * allocator used to managed memory of the data hold by the descriptor. */ \
+  /* This encodes both the presence of the DescriptorAddendum and the index of \
+   * the allocator used to managed memory of the data hold by the descriptor. \
+   */ \
   unsigned char extra;
 
 typedef struct CFI_cdesc_t {
diff --git a/flang/include/flang/Runtime/descriptor-consts.h b/flang/include/flang/Runtime/descriptor-consts.h
index acd7bc5ddbdef..b398e73613a3a 100644
--- a/flang/include/flang/Runtime/descriptor-consts.h
+++ b/flang/include/flang/Runtime/descriptor-consts.h
@@ -64,9 +64,11 @@ static constexpr RT_API_ATTRS std::size_t MaxDescriptorSizeInBytes(
   // }
   std::size_t bytes{24u + rank * 24u};
   if (addendum || lengthTypeParameters > 0) {
-    if (lengthTypeParameters < 1)
-      lengthTypeParameters = 1;
-    bytes += 8u + static_cast<std::size_t>(lengthTypeParameters) * 8u;
+    // DescriptorAddendum has base size of 16 bytes (pointer + 1
+    // TypeParameterValue) Additional len parameters beyond the first add 8
+    // bytes each
+    bytes += 16u +
+        static_cast<std::size_t>(std::max(lengthTypeParameters - 1, 0)) * 8u;
   }
   return bytes;
 }
diff --git a/flang/lib/Evaluate/type.cpp b/flang/lib/Evaluate/type.cpp
index 99dc8b1e5c676..8c62c52bfacb5 100644
--- a/flang/lib/Evaluate/type.cpp
+++ b/flang/lib/Evaluate/type.cpp
@@ -155,17 +155,25 @@ std::optional<Expr<SubscriptInteger>> DynamicType::GetCharLength() const {
 std::size_t DynamicType::GetAlignment(
     const TargetCharacteristics &targetCharacteristics) const {
   if (category_ == TypeCategory::Derived) {
+    // Polymorphic types (CLASS(*), CLASS(T)) store descriptors, not values
+    if (IsPolymorphic()) {
+      // Descriptor alignment (typically pointer-sized)
+      return targetCharacteristics.GetAlignment(TypeCategory::Integer, 8);
+    }
+    if (!derived_) {
+      return 1;
+    }
     switch (GetDerivedTypeSpec().category()) {
       SWITCH_COVERS_ALL_CASES
     case semantics::DerivedTypeSpec::Category::DerivedType:
-      if (derived_ && derived_->scope()) {
+      if (derived_->scope()) {
         return derived_->scope()->alignment().value_or(1);
       }
       break;
     case semantics::DerivedTypeSpec::Category::IntrinsicVector:
     case semantics::DerivedTypeSpec::Category::PairVector:
     case semantics::DerivedTypeSpec::Category::QuadVector:
-      if (derived_ && derived_->scope()) {
+      if (derived_->scope()) {
         return derived_->scope()->size();
       } else {
         common::die("Missing scope for Vector type.");
diff --git a/flang/lib/Semantics/runtime-type-info.cpp b/flang/lib/Semantics/runtime-type-info.cpp
index 2cee1e23646d0..86f5084525fcd 100644
--- a/flang/lib/Semantics/runtime-type-info.cpp
+++ b/flang/lib/Semantics/runtime-type-info.cpp
@@ -917,6 +917,13 @@ evaluate::StructureConstructor RuntimeTableBuilder::DescribeComponent(
   } else {
     AddValue(values, componentSchema_, "memoryspace"s, GetEnumValue("host"));
   }
+  // Store log2 of target-specific alignment for runtime offset computation
+  auto alignment{dyType.GetAlignment(foldingContext.targetCharacteristics())};
+  int log2Alignment{0};
+  while ((std::size_t{1} << log2Alignment) < alignment) {
+    ++log2Alignment;
+  }
+  AddValue(values, componentSchema_, "alignment"s, IntExpr<1>(log2Alignment));
   if (!hasDataInit) {
     AddValue(values, componentSchema_, "initialization"s,
         SomeExpr{evaluate::NullPointer{}});
diff --git a/flang/module/__fortran_type_info.f90 b/flang/module/__fortran_type_info.f90
index 0f1c3d018d009..36355b92efed2 100644
--- a/flang/module/__fortran_type_info.f90
+++ b/flang/module/__fortran_type_info.f90
@@ -83,9 +83,13 @@
   end enum
 
   enum, bind(c) ! common::TypeCategory
-    enumerator :: CategoryInteger = 0, CategoryReal = 1, &
-      CategoryComplex = 2, CategoryCharacter = 3, &
-      CategoryLogical = 4, CategoryDerived = 5
+    enumerator :: CategoryInteger   = 0, &
+                  CategoryUnsigned  = 1, & ! Unused, for C++ enum compat only
+                  CategoryReal      = 2, &
+                  CategoryComplex   = 3, &
+                  CategoryCharacter = 4, &
+                  CategoryLogical   = 5, &
+                  CategoryDerived   = 6
   end enum
 
   type :: Component ! data components, incl. object pointers
@@ -95,7 +99,8 @@
     integer(1) :: kind
     integer(1) :: rank
     integer(1) :: memorySpace ! Component::MemorySpace
-    integer(1) :: __padding0(3)
+    integer(1) :: alignment ! log2 of target-specific alignment
+    integer(1) :: __padding0(2)
     integer(kind=int64) :: offset
     type(Value) :: characterLen ! for category == Character
     type(DerivedType), pointer :: derived ! for category == Derived
diff --git a/flang/test/Lower/CUDA/cuda-allocatable-device.cuf b/flang/test/Lower/CUDA/cuda-allocatable-device.cuf
index 9c293872d1017..4b4291d8bc0d8 100644
--- a/flang/test/Lower/CUDA/cuda-allocatable-device.cuf
+++ b/flang/test/Lower/CUDA/cuda-allocatable-device.cuf
@@ -11,21 +11,21 @@ module m
     real(kind=8), pointer, dimension(:), device :: pd
   end type
 
-! SYMBOLS: .c.device_array, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:1_8 init:[component::component(name=.n.ad,genre=3_1,category=2_1,kind=8_1,rank=1_1,memoryspace=1_1,offset=0_8
+! SYMBOLS: .c.device_array, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:1_8 init:[component::component(name=.n.ad,genre=3_1,category=2_1,kind=8_1,rank=1_1,memoryspace=1_1,alignment=3_1,offset=0_8
 
   type managed_array
     real(kind=8), allocatable, dimension(:), managed :: ad
     real(kind=8), pointer, dimension(:), managed :: pd
   end type
 
-! SYMBOLS: .c.managed_array, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:1_8 init:[component::component(name=.n.ad,genre=3_1,category=2_1,kind=8_1,rank=1_1,memoryspace=2_1,offset=0_8
+! SYMBOLS: .c.managed_array, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:1_8 init:[component::component(name=.n.ad,genre=3_1,category=2_1,kind=8_1,rank=1_1,memoryspace=2_1,alignment=3_1,offset=0_8
 
   type unified_array
     real(kind=8), allocatable, dimension(:), unified :: ad
     real(kind=8), pointer, dimension(:), unified :: pd
   end type
 
-!  SYMBOLS: .c.unified_array, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:1_8 init:[component::component(name=.n.ad,genre=3_1,category=2_1,kind=8_1,rank=1_1,memoryspace=3_1
+!  SYMBOLS: .c.unified_array, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:1_8 init:[component::component(name=.n.ad,genre=3_1,category=2_1,kind=8_1,rank=1_1,memoryspace=3_1,alignment=3_1
 
   type(device_array), allocatable :: da(:)
   type(managed_array), allocatable :: ma(:)
diff --git a/flang/test/Lower/volatile-openmp.f90 b/flang/test/Lower/volatile-openmp.f90
index 107c53c629dab..bfe71e96572fe 100644
--- a/flang/test/Lower/volatile-openmp.f90
+++ b/flang/test/Lower/volatile-openmp.f90
@@ -23,9 +23,9 @@
 ! CHECK:           %[[VAL_11:.*]] = fir.address_of(@_QFEcontainer) : !fir.ref<!fir.type<_QFTt{array:!fir.box<!fir.ptr<!fir.array<?xi32>>>}>>
 ! CHECK:           %[[VAL_12:.*]] = fir.volatile_cast %[[VAL_11]] : (!fir.ref<!fir.type<_QFTt{array:!fir.box<!fir.ptr<!fir.array<?xi32>>>}>>) -> !fir.ref<!fir.type<_QFTt{array:!fir.box<!fir.ptr<!fir.array<?xi32>>>}>, volatile>
 ! CHECK:           %[[VAL_13:.*]]:2 = hlfir.declare %[[VAL_12]] {fortran_attrs = #fir.var_attrs<volatile>, uniq_name = "_QFEcontainer"} : (!fir.ref<!fir.type<_QFTt{array:!fir.box<!fir.ptr<!fir.array<?xi32>>>}>, volatile>) -> (!fir.ref<!fir.type<_QFTt{array:!fir.box<!fir.ptr<!fir.array<?xi32>>>}>, volatile>, !fir.ref<!fir.type<_QFTt{array:!fir.box<!fir.ptr<!fir.array<?xi32>>>}>, volatile>)
-! CHECK:           %[[VAL_14:.*]] = fir.address_of(@_QFE.c.t) : !fir.ref<!fir.array<1x!fir.type<_QM__fortran_type_infoTcomponent{name:!fir.box<!fir.ptr<!fir.char<1,?>>>,genre:i8,category:i8,kind:i8,rank:i8,memoryspace:i8,__padding0:!fir.array<3xi8>,offset:i64,characterlen:!fir.type<_QM__fortran_type_infoTvalue{{[<]?}}{genre:i8,__padding0:!fir.array<7xi8>,value:i64}{{[>]?}}>,derived:!fir.box<!fir.ptr<!fir.type<_QM__fortran_type_infoTderivedtype{binding:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTbinding{proc:!fir.type<_QM__fortran_builtinsT__builtin_c_funptr{__address:i64}>,name:!fir.box<!fir.ptr<!fir.char<1,?>>>}>>>>,name:!fir.box<!fir.ptr<!fir.char<1,?>>>,sizeinbytes:i64,uninstantiated:!fir.box<!fir.ptr<!fir.type<_QM__fortran_type_infoTderivedtype>>>,kindparameter:!fir.box<!fir.ptr<!fir.array<?xi64>>>,lenparameterkind:!fir.box<!fir.ptr<!fir.array<?xi8>>>,component:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTcomponent>>>>,procptr:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTprocptrcomponent{name:!fir.box<!fir.ptr<!fir.char<1,?>>>,offset:i64,initialization:!fir.type<_QM__fortran_builtinsT__builtin_c_funptr{__address:i64}>}>>>>,special:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTspecialbinding{{[<]?}}{which:i8,isargdescriptorset:i8,istypebound:i8,specialcaseflag:i8,__padding0:!fir.array<4xi8>,proc:!fir.type<_QM__fortran_builtinsT__builtin_c_funptr{__address:i64}>}{{[>]?}}>>>>,specialbitset:i32,hasparent:i8,noinitializationneeded:i8,nodestructionneeded:i8,nofinalizationneeded:i8,nodefinedassignment:i8,__padding0:!fir.array<3xi8>}>>>,lenvalue:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTvalue{{[<]?}}{genre:i8,__padding0:!fir.array<7xi8>,value:i64}{{[>]?}}>>>>,bounds:!fir.box<!fir.ptr<!fir.array<?x?x!fir.type<_QM__fortran_type_infoTvalue{{[<]?}}{genre:i8,__padding0:!fir.array<7xi8>,value:i64}{{[>]?}}>>>>,initialization:!fir.type<_QM__fortran_builtinsT__builtin_c_ptr{__address:i64}>}>>>
+! CHECK:           %[[VAL_14:.*]] = fir.address_of(@_QFE.c.t) : !fir.ref<!fir.array<1x!fir.type<_QM__fortran_type_infoTcomponent{name:!fir.box<!fir.ptr<!fir.char<1,?>>>,genre:i8,category:i8,kind:i8,rank:i8,memoryspace:i8,alignment:i8,__padding0:!fir.array<2xi8>,offset:i64,characterlen:!fir.type<_QM__fortran_type_infoTvalue{{[<]?}}{genre:i8,__padding0:!fir.array<7xi8>,value:i64}{{[>]?}}>,derived:!fir.box<!fir.ptr<!fir.type<_QM__fortran_type_infoTderivedtype{binding:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTbinding{proc:!fir.type<_QM__fortran_builtinsT__builtin_c_funptr{__address:i64}>,name:!fir.box<!fir.ptr<!fir.char<1,?>>>}>>>>,name:!fir.box<!fir.ptr<!fir.char<1,?>>>,sizeinbytes:i64,uninstantiated:!fir.box<!fir.ptr<!fir.type<_QM__fortran_type_infoTderivedtype>>>,kindparameter:!fir.box<!fir.ptr<!fir.array<?xi64>>>,lenparameterkind:!fir.box<!fir.ptr<!fir.array<?xi8>>>,component:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTcomponent>>>>,procptr:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTprocptrcomponent{name:!fir.box<!fir.ptr<!fir.char<1,?>>>,offset:i64,initialization:!fir.type<_QM__fortran_builtinsT__builtin_c_funptr{__address:i64}>}>>>>,special:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTspecialbinding{{[<]?}}{which:i8,isargdescriptorset:i8,istypebound:i8,specialcaseflag:i8,__padding0:!fir.array<4xi8>,proc:!fir.type<_QM__fortran_builtinsT__builtin_c_funptr{__address:i64}>}{{[>]?}}>>>>,specialbitset:i32,hasparent:i8,noinitializationneeded:i8,nodestructionneeded:i8,nofinalizationneeded:i8,nodefinedassignment:i8,__padding0:!fir.array<3xi8>}>>>,lenvalue:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTvalue{{[<]?}}{genre:i8,__padding0:!fir.array<7xi8>,value:i64}{{[>]?}}>>>>,bounds:!fir.box<!fir.ptr<!fir.array<?x?x!fir.type<_QM__fortran_type_infoTvalue{{[<]?}}{genre:i8,__padding0:!fir.array<7xi8>,value:i64}{{[>]?}}>>>>,initialization:!fir.type<_QM__fortran_builtinsT__builtin_c_ptr{__address:i64}>}>>>
 ! CHECK:           %[[VAL_15:.*]] = fir.shape_shift %[[VAL_0]], %[[VAL_1]] : (index, index) -> !fir.shapeshift<1>
-! CHECK:           %[[VAL_16:.*]]:2 = hlfir.declare %[[VAL_14]](%[[VAL_15]]) {fortran_attrs = #fir.var_attrs<target>, uniq_name = "_QFE.c.t"} : (!fir.ref<!fir.array<1x!fir.type<_QM__fortran_type_infoTcomponent{name:!fir.box<!fir.ptr<!fir.char<1,?>>>,genre:i8,category:i8,kind:i8,rank:i8,memoryspace:i8,__padding0:!fir.array<3xi8>,offset:i64,characterlen:!fir.type<_QM__fortran_type_infoTvalue{{[<]?}}{genre:i8,__padding0:!fir.array<7xi8>,value:i64}{{[>]?}}>,derived:!fir.box<!fir.ptr<!fir.type<_QM__fortran_type_infoTderivedtype{binding:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTbinding{proc:!fir.type<_QM__fortran_builtinsT__builtin_c_funptr{__address:i64}>,name:!fir.box<!fir.ptr<!fir.char<1,?>>>}>>>>,name:!fir.box<!fir.ptr<!fir.char<1,?>>>,sizeinbytes:i64,uninstantiated:!fir.box<!fir.ptr<!fir.type<_QM__fortran_type_infoTderivedtype>>>,kindparameter:!fir.box<!fir.ptr<!fir.array<?xi64>>>,lenparameterkind:!fir.box<!fir.ptr<!fir.array<?xi8>>>,component:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTcomponent>>>>,procptr:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTprocptrcomponent{name:!fir.box<!fir.ptr<!fir.char<1,?>>>,offset:i64,initialization:!fir.type<_QM__fortran_builtinsT__builtin_c_funptr{__address:i64}>}>>>>,special:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTspecialbinding{{[<]?}}{which:i8,isargdescriptorset:i8,istypebound:i8,specialcaseflag:i8,__padding0:!fir.array<4xi8>,proc:!fir.type<_QM__fortran_builtinsT__builtin_c_funptr{__address:i64}>}{{[>]?}}>>>>,specialbitset:i32,hasparent:i8,noinitializationneeded:i8,nodestructionneeded:i8,nofinalizationneeded:i8,nodefinedassignment:i8,__padding0:!fir.array<3xi8>}>>>,lenvalue:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTvalue{{[<]?}}{genre:i8,__padding0:!fir.array<7xi8>,value:i64}{{[>]?}}>>>>,bounds:!fir.box<!fir.ptr<!fir.array<?x?x!fir.type<_QM__fortran_type_infoTvalue{{[<]?}}{genre:i8,__padding0:!fir.array<7xi8>,value:i64}{{[>]?}}>>>>,initialization:!fir.type<_QM__fortran_builtinsT__builtin_c_ptr{__address:i64}>}>>>, !fir.shapeshift<1>) -> (!fir.box<{{.*}}>)
+! CHECK:           %[[VAL_16:.*]]:2 = hlfir.declare %[[VAL_14]](%[[VAL_15]]) {fortran_attrs = #fir.var_attrs<target>, uniq_name = "_QFE.c.t"} : (!fir.ref<!fir.array<1x!fir.type<_QM__fortran_type_infoTcomponent{name:!fir.box<!fir.ptr<!fir.char<1,?>>>,genre:i8,category:i8,kind:i8,rank:i8,memoryspace:i8,alignment:i8,__padding0:!fir.array<2xi8>,offset:i64,characterlen:!fir.type<_QM__fortran_type_infoTvalue{{[<]?}}{genre:i8,__padding0:!fir.array<7xi8>,value:i64}{{[>]?}}>,derived:!fir.box<!fir.ptr<!fir.type<_QM__fortran_type_infoTderivedtype{binding:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTbinding{proc:!fir.type<_QM__fortran_builtinsT__builtin_c_funptr{__address:i64}>,name:!fir.box<!fir.ptr<!fir.char<1,?>>>}>>>>,name:!fir.box<!fir.ptr<!fir.char<1,?>>>,sizeinbytes:i64,uninstantiated:!fir.box<!fir.ptr<!fir.type<_QM__fortran_type_infoTderivedtype>>>,kindparameter:!fir.box<!fir.ptr<!fir.array<?xi64>>>,lenparameterkind:!fir.box<!fir.ptr<!fir.array<?xi8>>>,component:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTcomponent>>>>,procptr:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTprocptrcomponent{name:!fir.box<!fir.ptr<!fir.char<1,?>>>,offset:i64,initialization:!fir.type<_QM__fortran_builtinsT__builtin_c_funptr{__address:i64}>}>>>>,special:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTspecialbinding{{[<]?}}{which:i8,isargdescriptorset:i8,istypebound:i8,specialcaseflag:i8,__padding0:!fir.array<4xi8>,proc:!fir.type<_QM__fortran_builtinsT__builtin_c_funptr{__address:i64}>}{{[>]?}}>>>>,specialbitset:i32,hasparent:i8,noinitializationneeded:i8,nodestructionneeded:i8,nofinalizationneeded:i8,nodefinedassignment:i8,__padding0:!fir.array<3xi8>}>>>,lenvalue:!fir.box<!fir.ptr<!fir.array<?x!fir.type<_QM__fortran_type_infoTvalue{{[<]?}}{genre:i8,__padding0:!fir.array<7xi8>,value:i64}{{[>]?}}>>>>,bounds:!fir.box<!fir.ptr<!fir.array<?x?x!fir.type<_QM__fortran_type_infoTvalue{{[<]?}}{genre:i8,__padding0:!fir.array<7xi8>,value:i64}{{[>]?}}>>>>,initialization:!fir.type<_QM__fortran_builtinsT__builtin_c_ptr{__address:i64}>}>>>, !fir.shapeshift<1>) -> (!fir.box<{{.*}}>)
 ! CHECK:           %[[VAL_17:.*]] = fir.address_of(@_QFE.dt.t) : !fir.ref<{{.*}}>
 ! CHECK:           %[[VAL_18:.*]]:2 = hlfir.declare %[[VAL_17]] {fortran_attrs = #fir.var_attrs<target>, uniq_name = "_QFE.dt.t"}
 ! CHECK:           %[[VAL_19:.*]] = hlfir.designate %[[VAL_13]]#0{"array"}   {fortran_attrs = #fir.var_attrs<pointer>} : (!fir.ref<!fir.type<_QFTt{array:!fir.box<!fir.ptr<!fir.array<?xi32>>>}>, volatile>) -> !fir.ref<!fir.box<!fir.ptr<!fir.array<?xi32>>>, volatile>
diff --git a/flang/test/Semantics/typeinfo01.f90 b/flang/test/Semantics/typeinfo01.f90
index bc433a131695a..c548280ea3851 100644
--- a/flang/test/Semantics/typeinfo01.f90
+++ b/flang/test/Semantics/typeinfo01.f90
@@ -7,7 +7,7 @@ module m01
     integer :: n
   end type
 !CHECK: Module scope: m01
-!CHECK: .c.t1, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:0_8 init:[component::component(name=.n.n,genre=1_1,category=0_1,kind=4_1,rank=0_1,memoryspace=0_1,offset=0_8,characterlen=value(genre=1_1,value=0_8),derived=NULL(),lenvalue=NULL(),bounds=NULL(),initialization=NULL())]
+!CHECK: .c.t1, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:0_8 init:[component::component(name=.n.n,genre=1_1,category=0_1,kind=4_1,rank=0_1,memoryspace=0_1,alignment=2_1,offset=0_8,characterlen=value(genre=1_1,value=0_8),derived=NULL(),lenvalue=NULL(),bounds=NULL(),initialization=NULL())]
 !CHECK: .dt.t1, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(derivedtype) init:derivedtype(binding=NULL(),name=.n.t1,sizeinbytes=4_8,uninstantiated=NULL(),kindparameter=NULL(),lenparameterkind=NULL(),component=.c.t1,procptr=NULL(),special=NULL(),specialbitset=0_4,hasparent=0_1,noinitializationneeded=1_1,nodestructionneeded=1_1,nofinalizationneeded=1_1,nodefinedassignment=1_1)
 !CHECK: .n.n, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: CHARACTER(1_8,1) init:"n"
 !CHECK: .n.t1, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: CHARACTER(2_8,1) init:"t1"
@@ -21,8 +21,8 @@ module m02
   type, extends(parent) :: child
     integer :: cn
   end type
-!CHECK: .c.child, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:1_8 init:[component::component(name=.n.parent,genre=1_1,category=6_1,kind=0_1,rank=0_1,memoryspace=0_1,offset=0_8,characterlen=value(genre=1_1,value=0_8),derived=.dt.parent,lenvalue=NULL(),bounds=NULL(),initialization=NULL()),component(name=.n.cn,genre=1_1,category=0_1,kind=4_1,rank=0_1,memoryspace=0_1,offset=4_8,characterlen=value(genre=1_1,value=0_8),derived=NULL(),lenvalue=NULL(),bounds=NULL(),initialization=NULL())]
-!CHECK: .c.parent, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:0_8 init:[component::component(name=.n.pn,genre=1_1,category=0_1,kind=4_1,rank=0_1,memoryspace=0_1,offset=0_8,characterlen=value(genre=1_1,value=0_8),derived=NULL(),lenvalue=NULL(),bounds=NULL(),initialization=NULL())]
+!CHECK: .c.child, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:1_8 init:[component::component(name=.n.parent,genre=1_1,category=6_1,kind=0_1,rank=0_1,memoryspace=0_1,alignment=2_1,offset=0_8,characterlen=value(genre=1_1,value=0_8),derived=.dt.parent,lenvalue=NULL(),bounds=NULL(),initialization=NULL()),component(name=.n.cn,genre=1_1,category=0_1,kind=4_1,rank=0_1,memoryspace=0_1,alignment=2_1,offset=4_8,characterlen=value(genre=1_1,value=0_8),derived=NULL(),lenvalue=NULL(),bounds=NULL(),initialization=NULL())]
+!CHECK: .c.parent, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:0_8 init:[component::component(name=.n.pn,genre=1_1,category=0_1,kind=4_1,rank=0_1,memoryspace=0_1,alignment=2_1,offset=0_8,characterlen=value(genre=1_1,value=0_8),derived=NULL(),lenvalue=NULL(),bounds=NULL(),initialization=NULL())]
 !CHECK: .dt.child, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(derivedtype) init:derivedtype(binding=NULL(),name=.n.child,sizeinbytes=8_8,uninstantiated=NULL(),kindparameter=NULL(),lenparameterkind=NULL(),component=.c.child,procptr=NULL(),special=NULL(),specialbitset=0_4,hasparent=1_1,noinitializationneeded=1_1,nodestructionneeded=1_1,nofinalizationneeded=1_1,nodefinedassignment=1_1)
 !CHECK: .dt.parent, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(derivedtype) init:derivedtype(binding=NULL(),name=.n.parent,sizeinbytes=4_8,uninstantiated=NULL(),kindparameter=NULL(),lenparameterkind=NULL(),component=.c.parent,procptr=NULL(),special=NULL(),specialbitset=0_4,hasparent=0_1,noinitializationneeded=1_1,nodestructionneeded=1_1,nofinalizationneeded=1_1,nodefinedassignment=1_1)
 end module
@@ -33,7 +33,7 @@ module m03
     real(kind=k) :: a
   end type
   type(kpdt(4)) :: x
-!CHECK: .c.kpdt.4, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:0_8 init:[component::component(name=.n.a,genre=1_1,category=2_1,kind=4_1,rank=0_1,memoryspace=0_1,offset=0_8,characterlen=value(genre=1_1,value=0_8),derived=NULL(),lenvalue=NULL(),bounds=NULL(),initialization=NULL())]
+!CHECK: .c.kpdt.4, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:0_8 init:[component::component(name=.n.a,genre=1_1,category=2_1,kind=4_1,rank=0_1,memoryspace=0_1,alignment=2_1,offset=0_8,characterlen=value(genre=1_1,value=0_8),derived=NULL(),lenvalue=NULL(),bounds=NULL(),initialization=NULL())]
 !CHECK: .dt.kpdt, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(derivedtype) init:derivedtype(name=.n.kpdt,uninstantiated=NULL(),kindparameter=.kp.kpdt,lenparameterkind=NULL())
 !CHECK: .dt.kpdt.4, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(derivedtype) init:derivedtype(binding=NULL(),name=.n.kpdt,sizeinbytes=4_8,uninstantiated=.dt.kpdt,kindparameter=.kp.kpdt.4,lenparameterkind=NULL(),component=.c.kpdt.4,procptr=NULL(),special=NULL(),specialbitset=0_4,hasparent=0_1,noinitializationneeded=1_1,nodestructionneeded=1_1,nofinalizationneeded=1_1,nodefinedassignment=1_1)
 !CHECK: .kp.kpdt.4, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: INTEGER(8) shape: 0_8:0_8 init:[INTEGER(8)::4_8]
@@ -84,7 +84,7 @@ subroutine s2(x, y)
     class(t2), intent(out) :: x
     class(t), intent(in) :: y
   end subroutine
-!CHECK: .c.t2, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:0_8 init:[component::component(name=.n.t,genre=1_1,category=6_1,kind=0_1,rank=0_1,memoryspace=0_1,offset=0_8,characterlen=value(genre=1_1,value=0_8),derived=.dt.t,lenvalue=NULL(),bounds=NULL(),initialization=NULL())]
+!CHECK: .c.t2, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:0_8 init:[component::component(name=.n.t,genre=1_1,category=6_1,kind=0_1,rank=0_1,memoryspace=0_1,alignment=0_1,offset=0_8,characterlen=value(genre=1_1,value=0_8),derived=.dt.t,lenvalue=NULL(),bounds=NULL(),initialization=NULL())]
 !CHECK: .dt.t, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(derivedtype) init:derivedtype(binding=.v.t,name=.n.t,sizeinbytes=0_8,uninstantiated=NULL(),kindparameter=NULL(),lenparameterkind=NULL(),component=NULL(),procptr=NULL(),special=.s.t,specialbitset=2_4,hasparent=0_1,noinitializationneeded=1_1,nodestructionneeded=1_1,nofinalizationneeded=1_1,nodefinedassignment=0_1)
 !CHECK: .dt.t2, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(derivedtype) init:derivedtype(binding=.v.t2,name=.n.t2,sizeinbytes=0_8,uninstantiated=NULL(),kindparameter=NULL(),lenparameterkind=NULL(),component=.c.t2,procptr=NULL(),special=.s.t2,specialbitset=2_4,hasparent=1_1,noinitializationneeded=1_1,nodestructionneeded=1_1,nofinalizationneeded=1_1,nodefinedassignment=0_1)
 !CHECK: .s.t, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(specialbinding) shape: 0_8:0_8 init:[specialbinding::specialbinding(which=1_1,isargdescriptorset=3_1,istypebound=1_1,specialcaseflag=0_1,proc=s1)]
@@ -112,7 +112,7 @@ subroutine s2(x, y)
     class(t), intent(out) :: x
     class(t2), intent(in) :: y
   end subroutine
-!CHECK: .c.t2, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:0_8 init:[component::component(name=.n.t,genre=1_1,category=6_1,kind=0_1,rank=0_1,memoryspace=0_1,offset=0_8,characterlen=value(genre=1_1,value=0_8),derived=.dt.t,lenvalue=NULL(),bounds=NULL(),initialization=NULL())]
+!CHECK: .c.t2, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:0_8 init:[component::component(name=.n.t,genre=1_1,category=6_1,kind=0_1,rank=0_1,memoryspace=0_1,alignment=0_1,offset=0_8,characterlen=value(genre=1_1,value=0_8),derived=.dt.t,lenvalue=NULL(),bounds=NULL(),initialization=NULL())]
 !CHECK: .dt.t, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(derivedtype) init:derivedtype(binding=.v.t,name=.n.t,sizeinbytes=0_8,uninstantiated=NULL(),kindparameter=NULL(),lenparameterkind=NULL(),component=NULL(),procptr=NULL(),special=.s.t,specialbitset=2_4,hasparent=0_1,noinitializationneeded=1_1,nodestructionneeded=1_1,nofinalizationneeded=1_1,nodefinedassignment=0_1)
 !CHECK: .dt.t2, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(derivedtype) init:derivedtype(binding=.v.t2,name=.n.t2,sizeinbytes=0_8,uninstantiated=NULL(),kindparameter=NULL(),lenparameterkind=NULL(),component=.c.t2,procptr=NULL(),special=.s.t2,specialbitset=2_4,hasparent=1_1,noinitializationneeded=1_1,nodestructionneeded=1_1,nofinalizationneeded=1_1,nodefinedassignment=0_1)
 !CHECK: .s.t, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(specialbinding) shape: 0_8:0_8 init:[specialbinding::specialbinding(which=1_1,isargdescriptorset=3_1,istypebound=1_1,specialcaseflag=0_1,proc=s1)]
@@ -260,7 +260,7 @@ module m11
     real :: automatic(len)
   end type
 !CHECK: .b.t.automatic, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(value) shape: 0_8:1_8,0_8:0_8 init:reshape([value::value(genre=2_1,value=1_8),value(genre=3_1,value=0_8)],shape=[2,1])
-!CHECK: .c.t, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:3_8 init:[component::component(name=.n.allocatable,genre=3_1,category=2_1,kind=4_1,rank=1_1,memoryspace=0_1,offset=0_8,characterlen=value(genre=1_1,value=0_8),derived=NULL(),lenvalue=NULL(),bounds=NULL(),initialization=NULL()),component(name=.n.pointer,genre=2_1,category=2_1,kind=4_1,rank=0_1,memoryspace=0_1,offset=48_8,characterlen=value(genre=1_1,value=0_8),derived=NULL(),lenvalue=NULL(),bounds=NULL(),initialization=.di.t.pointer),component(name=.n.chauto,genre=4_1,category=4_1,kind=1_1,rank=0_1,memoryspace=0_1,offset=72_8,characterlen=value(genre=3_1,value=0_8),derived=NULL(),lenvalue=NULL(),bounds=NULL(),initialization=NULL()),component(name=.n.automatic,genre=4_1,category=2_1,kind=4_1,rank=1_1,memoryspace=0_1,offset=96_8,characterlen=value(genre=1_1,value=0_8),derived=NULL(),lenvalue=NULL(),bounds=.b.t.automatic,initialization=NULL())]
+!CHECK: .c.t, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:3_8 init:[component::component(name=.n.allocatable,genre=3_1,category=2_1,kind=4_1,rank=1_1,memoryspace=0_1,alignment=2_1,offset=0_8,characterlen=value(genre=1_1,value=0_8),derived=NULL(),lenvalue=NULL(),bounds=NULL(),initialization=NULL()),component(name=.n.pointer,genre=2_1,category=2_1,kind=4_1,rank=0_1,memoryspace=0_1,alignment=2_1,offset=48_8,characterlen=value(genre=1_1,value=0_8),derived=NULL(),lenvalue=NULL(),bounds=NULL(),initialization=.di.t.pointer),component(name=.n.chauto,genre=4_1,category=4_1,kind=1_1,rank=0_1,memoryspace=0_1,alignment=0_1,offset=72_8,characterlen=value(genre=3_1,value=0_8),derived=NULL(),lenvalue=NULL(),bounds=NULL(),initialization=NULL()),component(name=.n.automatic,genre=4_1,category=2_1,kind=4_1,rank=1_1,memoryspace=0_1,alignment=2_1,offset=96_8,characterlen=value(genre=1_1,value=0_8),derived=NULL(),lenvalue=NULL(),bounds=.b.t.automatic,initialization=NULL())]
 !CHECK: .di.t.pointer, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(.dp.t.pointer) init:.dp.t.pointer(pointer=target)
 !CHECK: .dp.t.pointer (CompilerCreated): DerivedType components: pointer
 !CHECK: .dt.t, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(derivedtype) init:derivedtype(binding=NULL(),name=.n.t,sizeinbytes=144_8,uninstantiated=NULL(),kindparameter=NULL(),lenparameterkind=.lpk.t,component=.c.t,procptr=NULL(),special=NULL(),specialbitset=0_4,hasparent=0_1,noinitializationneeded=0_1,nodestructionneeded=0_1,nofinalizationneeded=1_1,nodefinedassignment=1_1)
diff --git a/flang/test/Semantics/typeinfo08.f90 b/flang/test/Semantics/typeinfo08.f90
index 5da31ad64e55a..92dd2535a89b9 100644
--- a/flang/test/Semantics/typeinfo08.f90
+++ b/flang/test/Semantics/typeinfo08.f90
@@ -12,7 +12,7 @@ module m
 end module
 
 !CHECK: Module scope: m size=0 alignment=1 sourceRange=113 bytes
-!CHECK: .c.s, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:0_8 init:[component::component(name=.n.t1,genre=1_1,category=6_1,kind=0_1,rank=0_1,memoryspace=0_1,offset=0_8,characterlen=value(genre=1_1,value=0_8),lenvalue=NULL(),bounds=NULL(),initialization=NULL())]
+!CHECK: .c.s, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(component) shape: 0_8:0_8 init:[component::component(name=.n.t1,genre=1_1,category=6_1,kind=0_1,rank=0_1,memoryspace=0_1,alignment=0_1,offset=0_8,characterlen=value(genre=1_1,value=0_8),lenvalue=NULL(),bounds=NULL(),initialization=NULL())]
 !CHECK: .dt.s, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: TYPE(derivedtype) init:derivedtype(binding=NULL(),name=.n.s,sizeinbytes=0_8,uninstantiated=NULL(),kindparameter=NULL(),lenparameterkind=.lpk.s,component=.c.s,procptr=NULL(),special=NULL(),specialbitset=0_4,hasparent=0_1,noinitializationneeded=1_1,nodestructionneeded=1_1,nofinalizationneeded=1_1,nodefinedassignment=1_1)
 !CHECK: .lpk.s, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: INTEGER(1) shape: 0_8:0_8 init:[INTEGER(1)::4_1]
 !CHECK: .n.s, SAVE, TARGET (CompilerCreated, ReadOnly): ObjectEntity type: CHARACTER(1_8,1) init:"s"

>From fc3bea6231ce4f258d15826ad0a687f9d565f1be Mon Sep 17 00:00:00 2001
From: "Ted M. Johnson" <tedmjohnson at protonmail.com>
Date: Wed, 11 Feb 2026 13:41:18 -0700
Subject: [PATCH 2/4] Add initial PDT Concrete Type doc to Sphinx toctree

I never build Documentation locally, so I didn't notice the
issue. Fixed.
---
 flang/docs/PDT_ConcreteTypes.md | 173 ++++++++++----------------------
 flang/docs/index.md             |   1 +
 2 files changed, 53 insertions(+), 121 deletions(-)

diff --git a/flang/docs/PDT_ConcreteTypes.md b/flang/docs/PDT_ConcreteTypes.md
index 0d49f5d5e8890..e938f0b9a660f 100644
--- a/flang/docs/PDT_ConcreteTypes.md
+++ b/flang/docs/PDT_ConcreteTypes.md
@@ -1,15 +1,16 @@
-# PDT Implementation: Concrete Type Instantiation
+<!--===- docs/PDT_ConcreteTypes.md
 
-This document describes the design for implementing Parameterized Derived Types
-(PDTs) with LEN type parameters in Flang using a "concrete type" approach. This
-strategy resolves LEN-dependent type layouts at runtime and caches the results,
-combining the flexibility of runtime evaluation with the performance of
-pre-computed offsets.
+   Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+   See https://llvm.org/LICENSE.txt for license information.
+   SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
+-->
 
-## Problem Statement
+# Parameterized Derived Types with LEN Type Parameters
 
-For PDTs with LEN parameters, component offsets depend on runtime values:
+## The LEN Type Parameter Runtime Problem
+
+For PDTs with LEN parameters, component offsets can depend either entirely or partially on runtime values:
 
 ```fortran
 ! Simple PDT with LEN type parameter
@@ -37,19 +38,13 @@ type :: nestedType(A, B)
 end type
 ```
 
-The `after_str` component's offset cannot be determined at compile time because
-`N` is a LEN parameter whose value is only known on instantiation.
 
 
-## Solution: Concrete Type Instantiation
+## Proposed Solution: Concrete Type Instantiation
 
-The concrete type approach creates resolved type descriptions lazily at runtime
-and caches them. All instances with identical LEN values share the same
-concrete type, providing O(1) component access after initial instantiation.
+The concrete type approach creates resolved type descriptions lazily at runtime and caches them. All instances with identical LEN values share the same concrete type, providing O(1) component access after initial instantiation.
 
-For any specific set of LEN values, the type layout is fully determined. All
-instances of `pdt(10)` have identical component offsets. Rather than
-recomputing offsets on each access, we resolve them once and cache the result.
+For any specific set of LEN values, the type layout is fully determined. For example, all instances of `pdt(10)` will have identical component offsets. Rather than recomputing offsets on each access, we resolve them once and cache the result.
 
 
 ### Architecture Overview
@@ -143,11 +138,12 @@ class Component {
 };
 ```
 
-The `characterLen_` and `bounds_` fields use `Value` to express LEN-dependent
-sizes. The `alignment_` field stores `log2(alignment)` for correct layout
-computation on heterogeneous systems (CPU/GPU). We add a public method for
-changing `offset_` so we can create the ConcreteType members with proper
-offsets.
+The `characterLen_` field and the `Value` elements within `bounds_` use the `Value::Genre::LenParameter` mechanism to express LEN-dependent sizes and extents.
+
+The `alignment_` field stores `log2(alignment)` for correct layout
+computation on heterogeneous systems (CPU/GPU).
+
+We also add/expose a public method for changing `offset_` so we can create the ConcreteType members with proper offsets.
 
 ### DescriptorAddendum
 
@@ -161,25 +157,16 @@ class DescriptorAddendum {
 };
 ```
 
-The `len_` field is changed from a fixed-size `TypeParameterValue len_[1]`
-array to `FlexibleArray<TypeParameterValue>`. This template wraps a single
-inline element `len_ntry_` and provides `operator[]` via pointer arithmetic,
-emulating a C flexible array member. Note that this is a different
-`FlexibleArray` from the one in `ISO_Fortran_binding.h` used for `dim`;
-that version inherits from `T` and indexes via `this`-pointer arithmetic. The
-`DescriptorAddendum::SizeInBytes()` calculation accounts for the one
-inline element: additional LEN parameters beyond the first require trailing
-storage allocated immediately after the struct.
+The `len_` field is changed from a fixed-size `TypeParameterValue len_[1]` array to `FlexibleArray<TypeParameterValue>`. This template wraps a single inline element `len_ntry_` and provides `operator[]` via pointer arithmetic in order to emulate a C flexible array member.
+
+Note that this is a slightly different `FlexibleArray` from the one in `ISO_Fortran_binding.h` used for `dim`; that version inherits from `T` and indexes via `this`-pointer arithmetic. 
 
 
 ## KIND Type Parameter Instantiation
 
-Flang already uses an instantiation pattern for KIND type parameters that serves
-as our initial model for LEN parameter handling.
+Flang already uses an instantiation pattern for KIND type parameters that serves as our initial model for LEN parameter handling.
 
-For KIND parameters, the compiler generates a separate `DerivedType` at compile
-time for each unique KIND instantiation. Each instantiated type points back to
-the original uninstantiated type via `uninstantiated_`:
+For KIND parameters, the compiler generates a separate `DerivedType` at compile time for each unique KIND instantiation. Each instantiated type points back to the original uninstantiated type via `uninstantiated_`:
 
 ```
 +--------------------+                  +---------------------------+
@@ -202,14 +189,13 @@ the original uninstantiated type via `uninstantiated_`:
 +-------------------+                  +----------------------+
 ```
 
-Descriptors A and B both store the same pointer (`0x7fff8a001000`) in
-their `derivedType_` field, referencing a single shared generic `DerivedType`
-structure.
+Here, descriptors `A` and `B` both store the same pointer (`0x7fff8a001000`) in their `derivedType_` field, referencing a single shared generic `DerivedType` structure.
 
-The `SameTypeAs` intrinsic in `derived-api` uses this pattern: if direct pointer comparison fails, it compares `uninstantiatedType()` pointers. This allows type matching across module boundaries.
+The `SameTypeAs` intrinsic in `derived-api` uses this pattern:
+if direct pointer comparison fails, it compares `uninstantiatedType()`
+pointers which allows for Type matching across module boundaries.
 
-For LEN parameters, we apply the same pattern but create concrete types at
-runtime:
+For LEN parameters, we apply the same pattern but create concrete types at runtime:
 
 - Concrete type's `uninstantiated_` points to the generic type
 - Cache key: `(genericType, len_values...)` for uniqueing the DerivedType
@@ -229,19 +215,14 @@ GetConcreteType(const DerivedType &genericType,
                 Terminator &terminator);
 ```
 
-1. If `genericType.LenParameters() == 0`, return `&genericType` (no LEN params)
-2. If `genericType.uninstantiatedType() != &genericType`, return `&genericType`
-   (already a concrete type from a previous call)
+1. If `genericType.LenParameters() == 0`, return `&genericType`
+2. If `genericType.uninstantiatedType() != &genericType`, it is already a Concrete type; return `&genericType`
 3. Compute hash from `(genericType*, len_values...)`
 4. Check cache; if found, return cached concrete type
 5. Create new concrete type with resolved offsets
 6. Insert into cache; return pointer
 
-Step 2 handles the case where a descriptor's `derivedType_` was previously set
-to a concrete type, and a subsequent operation (e.g., `AllocatableAllocate`)
-passes that already-resolved type back to `GetConcreteType`. Concrete types
-have `uninstantiatedType_` pointing to the original generic; generic types
-point to themselves.
+Step 2 handles the case where a descriptor's `derivedType_` was previously set to a concrete type, and a subsequent operation (e.g., `AllocatableAllocate`) passes that already-resolved type back to `GetConcreteType`. Concrete types have `uninstantiatedType_` pointing to the original generic; generic types point to themselves.
 
 
 ### Concrete Type Creation
@@ -249,13 +230,12 @@ point to themselves.
 When creating a new concrete type:
 
 1. Allocate `sizeof(DerivedType) + numComponents * sizeof(Component)`
-2. `memcpy` the generic type (Assumption: generic uses immutable metadata)
+2. `memcpy` the generic type (**Assumption: generic uses immutable metadata**)
 3. Duplicate the components of the `Component[]` array
 4. Patch the pointer members of `DerivedType`:
    - Set `component_` descriptor to point to new Component array
    - Set `uninstantiated_` to point back to generic type
-5. For each component, resolve/compute actual offset based on
-   alignment and sizes (which themselves may depend on the LEN values)
+5. For each component, resolve/compute actual offset based on alignment and sizes
 6. Set sizeInBytes to the final padded size of the resolved type
 
 
@@ -264,19 +244,11 @@ When creating a new concrete type:
 Component offsets are computed sequentially, respecting alignment. Each
 component's size is determined by its genre:
 
-- **Non-Data genres** (Allocatable, Pointer, Automatic): the component stores
-  a Descriptor, so its size is `Descriptor::SizeInBytes(rank, ...)` and
-  alignment is `alignof(Descriptor)`.
-- **Nested PDT with LEN parameters** (Data genre, Derived category):
-  `GetConcreteType` is called recursively, depth-first, to resolve the inner
-  type. The resolved `sizeInBytes` is then used as the element size,
-  and the component's `derivedType_` pointer is updated to the concrete type.
-- **All other Data components** (intrinsic types, non-PDT derived, KIND-only
-  PDT): element byte size times element count.
+- **Non-Data genres** (Allocatable, Pointer, Automatic): the component stores a Descriptor, so its size is `Descriptor::SizeInBytes(rank, ...)` and alignment is `alignof(Descriptor)`.
+- **Nested PDT with LEN parameters** (Data genre, Derived category): `GetConcreteType` is called recursively, depth-first, to resolve the inner type. The resolved `sizeInBytes` is then used as the element size, and the component's `derivedType_` pointer is updated to the concrete type.
+- **All other Data components** (intrinsic types, non-PDT derived, KIND-only PDT): element byte size times element count.
 
-For array components, the element count is computed from the bounds (which
-may themselves be LEN-dependent `Value` expressions resolved against the
-parent descriptor).
+For array components, the element count is computed from the bounds - which may themselves be nested LEN-dependent `Value` expressions.
 
 ```cpp
 // Simplified view of ResolveComponentOffsets
@@ -292,13 +264,10 @@ sizeInBytes = alignTo(currentOffset, maxAlignment);
 
 ### Cache Design
 
-The cache uses a custom dynamically-resizing hash table (`ConcreteTypeCache`)
-that relies only on C-style memory management (`malloc`, `calloc`, `free`).
-The hash value is used directly as the lookup key to avoid heap allocation
-on every query:
+The cache uses a custom dynamically-sized hash table (`ConcreteTypeCache`) that relies only on C-style memory management (`malloc`, `calloc`, `free`), and uses the hash value directly as the lookup key:
 
 ```cpp
-// Hash combining formula based on Boost's hash_combine.
+// Hashing, based on Boost's hash_combine.
 std::uint64_t
 ComputeConcreteTypeHash(const DerivedType &genericType,
                         const DescriptorAddendum &addendum,
@@ -314,35 +283,19 @@ ComputeConcreteTypeHash(const DerivedType &genericType,
 }
 ```
 
-The hash table starts with 31 buckets (tunable at build time using
--DFLANG_RT_PDT_CACHE_INITIAL_BUCKET_CNT), each holding a linked list of
-`CacheEntry` nodes, and the bucket count doubles when the average load reaches
-2 entries per bucket (again, tunable at build time using the define
--DFLANG_RT_PDT_CACHE_MAX_LOAD_FACTOR). Bucket arrays are allocated with
-`std::calloc` while linked-list entries (`CacheEntry` nodes) use the runtime's
-`New<T>` / `FreeMemory` allocator. The concrete `DerivedType` objects themselves
-are allocated with `std::calloc`, not `New<T>`.
+The hash table starts with 31 buckets (currently tunable at build time using `-DFLANG_RT_PDT_CACHE_INITIAL_BUCKET_CNT`), each holding a linked list of `CacheEntry` nodes, and the bucket count doubles when the average load reaches 2 entries per bucket (again, currently tunable at build time using the define `-DFLANG_RT_PDT_CACHE_MAX_LOAD_FACTOR`).
+
+Bucket arrays are allocated with `std::calloc` while linked-list entries (`CacheEntry` nodes) use the runtime's `New<T>` / `FreeMemory` allocator. The concrete `DerivedType` objects themselves are allocated with `std::calloc`, not `New<T>`.
 
 Hash collisions are theoretically possible but extremely unlikely given a
-64-bit hash space. Note that `Find()` compares only hash values and does not
-verify the full key (`genericType` pointer + LEN values) after a match. A
-collision between two distinct `(genericType, len_values)` combinations would
-therefore silently return the wrong concrete type -- a correctness bug, not
-merely a performance issue. If collision resistance becomes a concern, a
-full-key equality check should be added to `Find()`.
+64-bit hash space. Note that `Find()` compares only hash values, and does not verify the full key (`genericType` pointer + LEN values) after a match. A collision between two distinct `(genericType, len_values)` combinations would therefore silently return the wrong concrete type - a correctness bug, not merely a performance issue. If collision resistance becomes a concern, a full-key equality check could be added to `Find()`.
 
-Concrete types are never freed (like generic types). The runtime currently
-assumes single-threaded allocation; if concurrent PDT allocation becomes a
-requirement, the existing `Lock` class from `lock.h` can be used.
+Concrete types are never freed. The runtime currently assumes single-threaded allocation; if concurrent PDT allocation becomes a requirement, the existing `Lock` class from `lock.h` could be used.
 
 
 ### Device Compilation
 
-GPU device code cannot use the host-side cache (which relies on `malloc` and
-dynamic data structures). When `RT_DEVICE_COMPILATION` is defined,
-`GetConcreteType` provides a pre-implementation stub that passes through types
-without LEN parameters or already-concrete types, and crashes for actual LEN
-instantiation requests:
+GPU device code cannot use the host-side cache (which relies on `malloc` and dynamic data structures). When `RT_DEVICE_COMPILATION` is defined, `GetConcreteType` currrently provides a pre-implementation stub that passes through types without LEN parameters or already-concrete types, and crashes for actual LEN instantiation requests:
 
 ```cpp
 #ifdef RT_DEVICE_COMPILATION
@@ -357,17 +310,15 @@ RT_API_ATTRS const DerivedType *GetConcreteType(...) {
 
 ## Lowering Strategy
 
-All LEN parameter expressions--even constant literals--are evaluated at runtime
-and stored in the descriptor's `len_` Flexible array. This uniform approach
-simplifies lowering by avoiding special-case code paths.
+All LEN parameter expressions are evaluated at runtime and stored in the descriptor's `len_` Flexible array.
 
 ```fortran
 type(pdt(4, n+1, m*2)) :: x
 ```
 
-Lowers to:
+will lower to:
 
-```mlir
+```
 %len0 = arith.constant 4
 %len1 = arith.addi %n, %c1
 %len2 = arith.muli %m, %c2
@@ -378,8 +329,8 @@ call @AllocatableSetDerivedLength(%desc, 2, %len2)
 call @AllocatableAllocate(%desc)  // internally calls GetConcreteType
 ```
 
-LEN parameters do not affect type identity.
-`SAME_TYPE_AS(pdt(4), pdt(2+2))` returns `.TRUE.` because both evaluate to the same LEN value and thus the same member offsets. The cache correctly de-duplicates based on the resolved values.
+Note that LEN parameters do not affect type identity within the cache. `SAME_TYPE_AS(pdt(4), pdt(2+2))` returns `.TRUE.` because both evaluate to the same LEN value and thus the same member offsets. Thus the Concrete type cache will automatically de-duplicate based on the resolved LEN parameter values.
+
 
 ## Integration Points
 
@@ -394,25 +345,5 @@ LEN parameters do not affect type identity.
 | `assign.cpp` | `AllocateAssignmentLHS` | Resolves for LHS reallocation |
 | `type-info.cpp` | `ResolveDerivedTypeForComponent` | Resolves nested PDT components within `EstablishDescriptor` |
 
-## Comparison of Approaches
-
-| Aspect | Outlined | Inlined | Concrete Types |
-|--------|----------|---------|----------------|
-| Compile-time work | High | Low | Low |
-| Runtime work | None | Per-access | One-time |
-| Runtime LEN values | Not possible | Supported | Supported |
-| Access performance | Fast | Slower | Fast |
-| Memory overhead | Per-instantiation | Per-component | Cache only |
-
-## Remaining Work
-
-The runtime implementation is complete.
-The following compiler-side work remains:
-
-1. For nested PDT components, verify the compiler emits `lenValue_` arrays
-   that map parent LEN parameters to child LEN parameters.
-
-2. Verify lowering correctly populates `DescriptorAddendum` LEN values via
-   `AllocatableSetDerivedLength` before calling allocation APIs.
-
-3. End-to-end testing with Fortran programs exercising PDTs with LEN parameters.
+## Version
+1.0 - Initial posting
\ No newline at end of file
diff --git a/flang/docs/index.md b/flang/docs/index.md
index 67df710113a92..54273325f3ed3 100644
--- a/flang/docs/index.md
+++ b/flang/docs/index.md
@@ -91,6 +91,7 @@ on how to get in touch with us and to learn more about the current status.
    Overview
    ParallelMultiImageFortranRuntime
    ParameterizedDerivedTypes
+   PDT_ConcreteTypes
    ParserCombinators
    Parsing
    PolymorphicEntities

>From a4e4f901332edb51a59065a012f261b911d152f6 Mon Sep 17 00:00:00 2001
From: "Ted M. Johnson" <tedmjohnson at protonmail.com>
Date: Wed, 11 Feb 2026 14:27:04 -0700
Subject: [PATCH 3/4] Address feedback

Formatting.
---
 flang-rt/include/flang-rt/runtime/type-info-cache.h | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/flang-rt/include/flang-rt/runtime/type-info-cache.h b/flang-rt/include/flang-rt/runtime/type-info-cache.h
index dc4ae424ed672..c1685ed1ead69 100644
--- a/flang-rt/include/flang-rt/runtime/type-info-cache.h
+++ b/flang-rt/include/flang-rt/runtime/type-info-cache.h
@@ -1,5 +1,4 @@
-//===-- include/flang-rt/runtime/type-info-cache.h ----------------*- C++
-//-*-===//
+//===-- include/flang-rt/runtime/type-info-cache.h --------------*- C++ -*-===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
@@ -10,8 +9,9 @@
 // Cache for concrete PDT (Parameterized Derived Type) instantiations.
 //
 // For types with LEN parameters, layout depends on runtime values. This cache
-// stores resolved "concrete types" keyed by (generic_type, len_values...) so
-// that all instances with identical LEN values share the same type description.
+// stores resolved "concrete types" keyed by (generic_type, len_values...)
+// so that all instances with identical LEN values share the same type
+// description.
 //
 //===----------------------------------------------------------------------===//
 

>From 512171a1f88e75c6344fe211bae38553c2bc8ea9 Mon Sep 17 00:00:00 2001
From: "Ted M. Johnson" <tedmjohnson at protonmail.com>
Date: Thu, 12 Feb 2026 19:17:20 -0700
Subject: [PATCH 4/4] [flang-rt] Address RFC review feedback for PDT concrete
 types

Fix derived type comparisons in ClassIs, ExtendsTypeOf, and SameTypeAs
to handle concrete vs generic type pointers via uninstantiatedType().
Fix MustDeallocateLHS to check LEN parameters unconditionally. Fix
GetConcreteType early-exit when uninstantiatedType() is null.

This change also addresses some pre-existing clang-tidy issues.
---
 flang-rt/lib/runtime/assign.cpp          | 31 ++++++-----
 flang-rt/lib/runtime/derived-api.cpp     | 71 ++++++++++++++----------
 flang-rt/lib/runtime/type-info-cache.cpp | 19 +++----
 flang/docs/PDT_ConcreteTypes.md          | 70 ++++++++++++++++++++++-
 4 files changed, 135 insertions(+), 56 deletions(-)

diff --git a/flang-rt/lib/runtime/assign.cpp b/flang-rt/lib/runtime/assign.cpp
index 249b5931b521a..c718b9a32cca2 100644
--- a/flang-rt/lib/runtime/assign.cpp
+++ b/flang-rt/lib/runtime/assign.cpp
@@ -49,13 +49,18 @@ static inline RT_API_ATTRS bool MustDeallocateLHS(
     if (toDerived != fromDerived) {
       return true;
     }
-    if (fromDerived) {
-      // Distinct LEN parameters? Deallocate
-      std::size_t lenParms{fromDerived->LenParameters()};
-      for (std::size_t j{0}; j < lenParms; ++j) {
-        if (toAddendum->LenParameterValue(j) !=
-            fromAddendum->LenParameterValue(j)) {
-          return true;
+    // Fall-thru to check LEN parameters regardless of polymorphicLHS
+  }
+  // Distinct LEN parameters? Deallocate, per F2018 10.2.1.3
+  if (const DescriptorAddendum *fromAddendum{from.Addendum()}) {
+    if (const typeInfo::DerivedType *fromDerived{fromAddendum->derivedType()}) {
+      if (std::size_t lenParms{fromDerived->LenParameters()}) {
+        DescriptorAddendum *toAddendum{to.Addendum()};
+        for (std::size_t j{0}; j < lenParms; ++j) {
+          if (toAddendum->LenParameterValue(j) !=
+              fromAddendum->LenParameterValue(j)) {
+            return true;
+          }
         }
       }
     }
@@ -532,13 +537,13 @@ RT_API_ATTRS int AssignTicket::Continue(WorkQueue &workQueue) {
     // tempDescriptor_ must outlive pending child ticket(s)
     done_ = true;
     return StatContinue;
-  } else {
-    if (toDeallocate_) {
-      toDeallocate_->Deallocate();
-      toDeallocate_ = nullptr;
-    }
-    return StatOk;
   }
+
+  if (toDeallocate_) {
+    toDeallocate_->Deallocate();
+    toDeallocate_ = nullptr;
+  }
+  return StatOk;
 }
 
 template <bool IS_COMPONENTWISE>
diff --git a/flang-rt/lib/runtime/derived-api.cpp b/flang-rt/lib/runtime/derived-api.cpp
index fe6868292f019..f74ff035a51b0 100644
--- a/flang-rt/lib/runtime/derived-api.cpp
+++ b/flang-rt/lib/runtime/derived-api.cpp
@@ -64,35 +64,44 @@ void RTDEF(Finalize)(
   }
 }
 
+static RT_API_ATTRS const typeInfo::DerivedType *GetDerivedType(
+    const Descriptor &desc) {
+  if (const DescriptorAddendum *addendum{desc.Addendum()}) {
+    if (const auto *derived{addendum->derivedType()}) {
+      return derived;
+    }
+  }
+  return nullptr;
+}
+
+static RT_API_ATTRS bool DerivedTypesMatch(
+    const typeInfo::DerivedType *a, const typeInfo::DerivedType *b) {
+  if (a == b) {
+    return true;
+  }
+  if (!a || !b) {
+    return false;
+  }
+  const auto *baseA{a->uninstantiatedType()};
+  const auto *baseB{b->uninstantiatedType()};
+  return (baseA ? baseA : a) == (baseB ? baseB : b);
+}
+
 bool RTDEF(ClassIs)(
     const Descriptor &descriptor, const typeInfo::DerivedType &derivedType) {
   if (const DescriptorAddendum * addendum{descriptor.Addendum()}) {
     if (const auto *derived{addendum->derivedType()}) {
-      if (derived == &derivedType) {
-        return true;
-      }
-      const typeInfo::DerivedType *parent{derived->GetParentType()};
-      while (parent) {
-        if (parent == &derivedType) {
+      for (const typeInfo::DerivedType *type{derived}; type;
+          type = type->GetParentType()) {
+        if (DerivedTypesMatch(type, &derivedType)) {
           return true;
         }
-        parent = parent->GetParentType();
       }
     }
   }
   return false;
 }
 
-static RT_API_ATTRS const typeInfo::DerivedType *GetDerivedType(
-    const Descriptor &desc) {
-  if (const DescriptorAddendum * addendum{desc.Addendum()}) {
-    if (const auto *derived{addendum->derivedType()}) {
-      return derived;
-    }
-  }
-  return nullptr;
-}
-
 bool RTDEF(SameTypeAs)(const Descriptor &a, const Descriptor &b) {
   auto aType{a.raw().type};
   auto bType{b.raw().type};
@@ -100,12 +109,14 @@ bool RTDEF(SameTypeAs)(const Descriptor &a, const Descriptor &b) {
       (bType != CFI_type_struct && bType != CFI_type_other)) {
     // If either type is intrinsic, they must match.
     return aType == bType;
-  } else if (const typeInfo::DerivedType * derivedTypeA{GetDerivedType(a)}) {
+  }
+  if (const typeInfo::DerivedType *derivedTypeA{GetDerivedType(a)}) {
     if (const typeInfo::DerivedType * derivedTypeB{GetDerivedType(b)}) {
       if (derivedTypeA == derivedTypeB) {
         return true;
-      } else if (const typeInfo::DerivedType *
-          uninstDerivedTypeA{derivedTypeA->uninstantiatedType()}) {
+      }
+      if (const typeInfo::DerivedType *uninstDerivedTypeA{
+              derivedTypeA->uninstantiatedType()}) {
         // There are KIND type parameters, are these the same type if those
         // are ignored?
         const typeInfo::DerivedType *uninstDerivedTypeB{
@@ -130,14 +141,15 @@ bool RTDEF(ExtendsTypeOf)(const Descriptor &a, const Descriptor &mold) {
       (moldType != CFI_type_struct && moldType != CFI_type_other)) {
     if (!mold.IsAllocated()) {
       return true;
-    } else if (!a.IsAllocated()) {
+    }
+    if (!a.IsAllocated()) {
       return false;
-    } else {
-      // If either type is intrinsic and not a pointer or allocatable
-      // then they must match.
-      return aType == moldType;
     }
-  } else if (const auto *derivedTypeMold{GetDerivedType(mold)}) {
+    // If either type is intrinsic and not a pointer or allocatable
+    // then they must match.
+    return aType == moldType;
+  }
+  if (const auto *derivedTypeMold{GetDerivedType(mold)}) {
     // If A is unlimited polymorphic and is either a disassociated pointer or
     // unallocated allocatable, the result is false.
     // Otherwise if the dynamic type of A or MOLD is extensible, the result is
@@ -145,15 +157,14 @@ bool RTDEF(ExtendsTypeOf)(const Descriptor &a, const Descriptor &mold) {
     // dynamic type of MOLD.
     for (const typeInfo::DerivedType *derivedTypeA{GetDerivedType(a)};
          derivedTypeA; derivedTypeA = derivedTypeA->GetParentType()) {
-      if (derivedTypeA == derivedTypeMold) {
+      if (DerivedTypesMatch(derivedTypeA, derivedTypeMold)) {
         return true;
       }
     }
     return false;
-  } else {
-    // MOLD is unlimited polymorphic and unallocated/disassociated.
-    return true;
   }
+  // MOLD is unlimited polymorphic and unallocated/disassociated.
+  return true;
 }
 
 void RTDEF(DestroyWithoutFinalization)(const Descriptor &descriptor) {
diff --git a/flang-rt/lib/runtime/type-info-cache.cpp b/flang-rt/lib/runtime/type-info-cache.cpp
index 2696b348f1261..7855a9e432548 100644
--- a/flang-rt/lib/runtime/type-info-cache.cpp
+++ b/flang-rt/lib/runtime/type-info-cache.cpp
@@ -27,8 +27,8 @@ RT_API_ATTRS const DerivedType *GetConcreteType(const DerivedType &genericType,
     const Descriptor &instance, runtime::Terminator &terminator) {
   std::size_t numLenParams = genericType.LenParameters();
   // Types without LEN params or already-concrete types can pass through
-  if ((numLenParams == 0) ||
-      (genericType.uninstantiatedType() != &genericType)) {
+  const DerivedType *uninst = genericType.uninstantiatedType();
+  if ((numLenParams == 0) || (uninst != nullptr && uninst != &genericType)) {
     return &genericType;
   }
   // Cannot instantiate PDT with LEN parameters on device
@@ -335,15 +335,12 @@ static DerivedType *CreateConcreteType(const DerivedType &generic,
 RT_API_ATTRS const DerivedType *GetConcreteType(const DerivedType &genericType,
     const Descriptor &instance, runtime::Terminator &terminator) {
   std::size_t numLenParams = genericType.LenParameters();
-  // Fast path: No LEN params, or we already have a concrete type.
-  // This occurs when the descriptor's derivedType_ was previously set
-  // to a concrete type, and a subsequent operation
-  // (e.g., AllocatableAllocate) calls GetConcreteType again with that
-  // already-resolved type. Concrete types have their uninstantiatedType_
-  // pointing to the original generic, not to themselves.
-  if ((numLenParams == 0) ||
-      (genericType.uninstantiatedType() != &genericType)) {
-    return &genericType; // Already concrete, return as-is
+  // Concrete types have uninstantiatedType_ pointing to the original generic.
+  // Generic types with LEN params have uninstantiatedType_ == nullptr.
+  // Generic types without LEN params are caught by the numLenParams == 0 check.
+  const DerivedType *uninst = genericType.uninstantiatedType();
+  if ((numLenParams == 0) || (uninst != nullptr && uninst != &genericType)) {
+    return &genericType; // Already concrete or no LEN params
   }
 
   // Check that instance has addendum with LEN values
diff --git a/flang/docs/PDT_ConcreteTypes.md b/flang/docs/PDT_ConcreteTypes.md
index e938f0b9a660f..f09604296abdf 100644
--- a/flang/docs/PDT_ConcreteTypes.md
+++ b/flang/docs/PDT_ConcreteTypes.md
@@ -262,6 +262,74 @@ for (Component &comp : components) {
 sizeInBytes = alignTo(currentOffset, maxAlignment);
 ```
 
+### Recursion and Self-Referential PDTs
+
+A PDT may reference itself through Pointer or Allocatable components:
+
+```fortran
+type :: trecurse(X)
+  integer, len :: X
+  type(trecurse(X+1)), pointer :: P
+end type
+```
+
+This does not cause infinite recursion in `GetConcreteType`; `P` has genre `Pointer`, not `Data`.
+During offset resolution, the genre determines how a component's size is computed:
+
+- Data genre: the component is stored inline, so its full size must be known.
+  For a nested PDT, this triggers a recursive `GetConcreteType` call to resolve the inner type.
+- Pointer/Allocatable genre: the component stores a fixed-size `Descriptor`,
+  regardless of the pointed-to type. The size is `Descriptor::SizeInBytes(rank, ...)`,
+  which depends only on rank and addendum — not on the target type's layout.
+
+```cpp
+    if (comp.genre() != Component::Genre::Data) {
+      // Non-Data genres (Allocatable, Pointer, Automatic): store a Descriptor
+      const DerivedType *derivedComp = comp.derivedType();
+      componentSize = Descriptor::SizeInBytes(
+          comp.rank(), true, derivedComp ? derivedComp->LenParameters() : 0);
+      alignment = alignof(Descriptor);
+    } else if (...)
+```
+
+Thus, when `GetConcreteType` resolves `trecurse` with `X=5`:
+
+```
+GetConcreteType(trecurse, instance with X=5)
+  --> ResolveComponentOffsets:
+        Component "P" (genre=Pointer, rank=0):
+          size = Descriptor::SizeInBytes(0, ...)  // fixed, ~48 bytes
+          // Does NOT recurse into trecurse(X+1)
+          // Does NOT evaluate X+1
+          // Does NOT call GetConcreteType for the pointed-to type
+        SetOffset(P, 0)
+      sizeInBytes = alignTo(48, maxAlignment)
+  --> done, no recursion
+```
+
+The concrete type for `trecurse(6)` is created lazily if/when `P` is actually allocated:
+
+```fortran
+type(trecurse(5)) :: foo
+allocate(foo%P)          !-- Runtime: AllocatableAllocate -> GetConcreteType(trecurse, X=6)
+allocate(foo%P%P)        !-- Runtime: GetConcreteType(trecurse, X=7)
+```
+
+Pointer association does not trigger concrete type creation at all — `foo%P => some_target` is a shallow descriptor copy handled by `PointerAssociate`.
+
+Note that the Fortran standard (C749) requires non-POINTER, non-ALLOCATABLE components to specify a previously defined type.
+A declaration like `type(trecurse(X+1)) :: P` (without `POINTER` or `ALLOCATABLE`) would be rejected at compile time due to
+the specified `type` not being previously defined.
+
+For the `X+1` expression, the component's `lenValue_` array encodes the mapping from parent LEN params to child LEN params. For a bare parameter reference like `type(trecurse(X))`, this is straightforward — `Value(genre=LenParameter, value=0)` referencing the parent's parameter index 0. However, `X+1` is an expression, not a bare parameter reference.
+
+Currently, the `Value` class can only encode `Genre::LenParameter` (a single parameter index) or `Genre::Explicit` (a constant). Encoding computed expressions which do not fold at compile time, like `X+1` but unlike `2+2`, is a known limitation within the front-end. To resolve this, the front-end may introduce anonymous LEN parameter slots in the descriptor's `len_[]` array to hold pre-computed expression results, allowing `lenValue_` entries to reference them via `Genre::LenParameter`.
+
+Regardless of this limitation, offset resolution for Pointer/Allocatable components is unaffected since their `lenValue_` arrays are not consulted during `ResolveComponentOffsets` — they are only relevant when the component is later allocated and the runtime needs to populate the child descriptor's `len_[]` values.
+
+For more information, see [this link](#recursion-and-self-referential-pdts).
+
+
 ### Cache Design
 
 The cache uses a custom dynamically-sized hash table (`ConcreteTypeCache`) that relies only on C-style memory management (`malloc`, `calloc`, `free`), and uses the hash value directly as the lookup key:
@@ -329,8 +397,6 @@ call @AllocatableSetDerivedLength(%desc, 2, %len2)
 call @AllocatableAllocate(%desc)  // internally calls GetConcreteType
 ```
 
-Note that LEN parameters do not affect type identity within the cache. `SAME_TYPE_AS(pdt(4), pdt(2+2))` returns `.TRUE.` because both evaluate to the same LEN value and thus the same member offsets. Thus the Concrete type cache will automatically de-duplicate based on the resolved LEN parameter values.
-
 
 ## Integration Points
 



More information about the flang-commits mailing list