[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