[flang-commits] [flang] [flang] Fix cycle detection in derived-type I/O checks (PR #213470)

via flang-commits flang-commits at lists.llvm.org
Sat Aug 1 10:25:23 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-flang-semantics

Author: RohithPariki

<details>
<summary>Changes</summary>

Fixes #<!-- -->213324

This fixes a bug where `allocatable` or `pointer` components in parameterized derived types were silently accepted in I/O statements. 

The root cause was that `VisitedSymbolSet` in `check-io.cpp` was being used as a global list of seen types. If a type was first encountered on a path with user-defined I/O, it was marked as visited and never erased. If we later hit the same type on a path without user-defined I/O, the compiler would skip checking it entirely.

This patch makes the visited set path-scoped by erasing the type symbol on unwind (matching how we handle this elsewhere in the file). I've also added the reproducer from the issue as a regression test.


---
Full diff: https://github.com/llvm/llvm-project/pull/213470.diff


2 Files Affected:

- (modified) flang/lib/Semantics/check-io.cpp (+26-12) 
- (added) flang/test/Semantics/io-bad-allocatable.f90 (+50) 


``````````diff
diff --git a/flang/lib/Semantics/check-io.cpp b/flang/lib/Semantics/check-io.cpp
index 5a5bee2b80e3c..87e61f3966171 100644
--- a/flang/lib/Semantics/check-io.cpp
+++ b/flang/lib/Semantics/check-io.cpp
@@ -1124,8 +1124,11 @@ using VisitedSymbolSet = std::unordered_set<const Symbol *>;
 
 // Seeks out an allocatable or pointer ultimate component that is not
 // nested in a nonallocatable/nonpointer component with a specific defined I/O
-// procedure. The 'visited' set tracks derived types to break cycles caused by
-// an illegal recursive type definition (F2023 C749).
+// procedure. The 'visited' set is path-scoped: a symbol is inserted on entry
+// and erased on exit so that only true ancestors on the current recursion path
+// are present. This breaks cycles caused by an illegal recursive type
+// definition (F2023 C749) while allowing shared (diamond) PDT instantiations
+// that use the same typeSymbol to be examined independently on each path.
 static const Symbol *FindUnsafeIoDirectComponent(common::DefinedIo which,
     const DerivedTypeSpec &derived, const Scope &scope,
     VisitedSymbolSet &visited) {
@@ -1133,13 +1136,15 @@ static const Symbol *FindUnsafeIoDirectComponent(common::DefinedIo which,
     return nullptr;
   }
   if (!visited.insert(&derived.typeSymbol()).second) {
-    return nullptr;
+    return nullptr; // true ancestor on this path -> genuine cycle
   }
+  const Symbol *result{nullptr};
   if (const Scope * dtScope{derived.scope()}) {
     for (const auto &pair : *dtScope) {
       const Symbol &symbol{*pair.second};
       if (IsAllocatableOrPointer(symbol)) {
-        return &symbol;
+        result = &symbol;
+        break;
       }
       if (const auto *details{symbol.detailsIf<ObjectEntityDetails>()}) {
         if (const DeclTypeSpec * type{details->type()}) {
@@ -1147,14 +1152,16 @@ static const Symbol *FindUnsafeIoDirectComponent(common::DefinedIo which,
             const DerivedTypeSpec &componentDerived{type->derivedTypeSpec()};
             if (const Symbol *bad{FindUnsafeIoDirectComponent(
                     which, componentDerived, scope, visited)}) {
-              return bad;
+              result = bad;
+              break;
             }
           }
         }
       }
     }
   }
-  return nullptr;
+  visited.erase(&derived.typeSymbol()); // path-scoped: erase on unwind
+  return result;
 }
 
 static const Symbol *FindUnsafeIoDirectComponent(common::DefinedIo which,
@@ -1165,14 +1172,18 @@ static const Symbol *FindUnsafeIoDirectComponent(common::DefinedIo which,
 
 // For a type that does not have a defined I/O subroutine, finds a direct
 // component that is a witness to an accessibility violation outside the module
-// in which the type was defined.  The 'visited' set tracks derived types to
-// break cycles caused by an illegal recursive type definition (F2023 C749).
+// in which the type was defined.  The 'visited' set is path-scoped: a symbol
+// is inserted on entry and erased on exit so that only true ancestors on the
+// current recursion path are present, breaking cycles caused by an illegal
+// recursive type definition (F2023 C749) while allowing shared PDT
+// instantiations with the same typeSymbol to be examined independently.
 static const Symbol *FindInaccessibleComponent(common::DefinedIo which,
     const DerivedTypeSpec &derived, const Scope &scope,
     VisitedSymbolSet &visited) {
   if (!visited.insert(&derived.typeSymbol()).second) {
-    return nullptr;
+    return nullptr; // true ancestor on this path -> genuine cycle
   }
+  const Symbol *result{nullptr};
   if (const Scope * dtScope{derived.scope()}) {
     if (const Scope * module{FindModuleContaining(*dtScope)}) {
       for (const auto &pair : *dtScope) {
@@ -1194,20 +1205,23 @@ static const Symbol *FindInaccessibleComponent(common::DefinedIo which,
           if (symbol.attrs().test(Attr::PRIVATE) &&
               !symbol.test(Symbol::Flag::ParentComp)) {
             if (!DoesScopeContain(module, scope)) {
-              return &symbol;
+              result = &symbol;
+              break;
             }
           }
           if (componentDerived) {
             if (const Symbol *bad{FindInaccessibleComponent(
                     which, *componentDerived, scope, visited)}) {
-              return bad;
+              result = bad;
+              break;
             }
           }
         }
       }
     }
   }
-  return nullptr;
+  visited.erase(&derived.typeSymbol()); // path-scoped: erase on unwind
+  return result;
 }
 
 static const Symbol *FindInaccessibleComponent(common::DefinedIo which,
diff --git a/flang/test/Semantics/io-bad-allocatable.f90 b/flang/test/Semantics/io-bad-allocatable.f90
new file mode 100644
index 0000000000000..b311a5d584eee
--- /dev/null
+++ b/flang/test/Semantics/io-bad-allocatable.f90
@@ -0,0 +1,50 @@
+! RUN: %python %S/test_errors.py %s %flang_fc1
+! Test for https://github.com/llvm/llvm-project/issues/213324
+! The 'visited' set in FindUnsafeIoDirectComponent used to be global ("seen
+! anywhere") rather than path-scoped.  When a PDT was first reached via a
+! shielded branch (with user-defined I/O), its typeSymbol was inserted and
+! never erased, so a later unshielded branch sharing the same typeSymbol was
+! silently pruned, producing a missed diagnostic.
+module m
+  type :: leaf(k)
+    integer, kind :: k = 2
+    real, allocatable :: a(:) ! the unsafe direct component
+  end type
+
+  interface write(unformatted)
+    module procedure wleaf1 ! matches leaf(1) only
+  end interface
+
+  type :: branch(k)
+    integer, kind :: k = 2
+    type(leaf(k)) :: item
+  end type
+
+  type :: container
+    type(branch(1)) :: a_safe ! visited first: leaf(1) is shielded
+    type(branch(2)) :: b_bad  ! previously pruned; leaf(2)'s allocatable missed
+  end type
+
+contains
+  subroutine wleaf1(dtv, unit, iostat, iomsg)
+    class(leaf(1)), intent(in) :: dtv
+    integer, intent(in) :: unit
+    integer, intent(out) :: iostat
+    character(*), intent(in out) :: iomsg
+    iostat = 0
+  end subroutine
+
+  subroutine bug_case(u)
+    integer, intent(in) :: u
+    type(container) :: z
+    !ERROR: Derived type 'container' in I/O cannot have an allocatable or pointer direct component 'a' unless using defined I/O
+    write(u) z
+  end subroutine
+
+  subroutine control_case(u)
+    integer, intent(in) :: u
+    type(branch(2)) :: y
+    !ERROR: Derived type 'branch(k=2_4)' in I/O cannot have an allocatable or pointer direct component 'a' unless using defined I/O
+    write(u) y
+  end subroutine
+end module

``````````

</details>


https://github.com/llvm/llvm-project/pull/213470


More information about the flang-commits mailing list