[flang-commits] [flang] [flang] Update PUBLIC and PRIVATE accessibility to Fortran 2018 (PR #177596)

via flang-commits flang-commits at lists.llvm.org
Tue May 5 14:02:40 PDT 2026


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

>From 7f0798d56c63d814cc6a6cc2f0a1ad73b748645a Mon Sep 17 00:00:00 2001
From: "Ted M. Johnson" <tedmjohnson at protonmail.com>
Date: Thu, 23 Apr 2026 17:15:29 -0600
Subject: [PATCH 1/2] [flang] Update PUBLIC and PRIVATE accessibility to
 Fortran 2018

This patch enforces PUBLIC and PRIVATE accessibility as defined
starting in F2018, and passed initially as paper J3/13-327r3;
the brief description below is from "What's New In Fortran 2018"
by John Reid:

    If a module A uses module B, the default accessibility for entities
    it accesses from B is that of A.  Specifying another accessibility
    for each entity is awkward and error prone. It is now possible
    for the name of a module to be included in the list of names of
    entities made public or private on a public or private statement.
    This sets the default for all entities accessed from that module.

In the F2023 standard, this is clause 8.6.1.
---
 flang/include/flang/Semantics/scope.h         | 14 ++++
 flang/lib/Semantics/resolve-names.cpp         | 72 +++++++++++++++++++
 .../Semantics/modfile-accessibility01.f90     | 27 +++++++
 .../Semantics/modfile-accessibility02.f90     | 26 +++++++
 .../Semantics/modfile-accessibility03.f90     | 14 ++++
 .../Semantics/modfile-accessibility04.f90     | 25 +++++++
 6 files changed, 178 insertions(+)
 create mode 100644 flang/test/Semantics/modfile-accessibility01.f90
 create mode 100644 flang/test/Semantics/modfile-accessibility02.f90
 create mode 100644 flang/test/Semantics/modfile-accessibility03.f90
 create mode 100644 flang/test/Semantics/modfile-accessibility04.f90

diff --git a/flang/include/flang/Semantics/scope.h b/flang/include/flang/Semantics/scope.h
index 16553fe692e79..c48cde86cfef2 100644
--- a/flang/include/flang/Semantics/scope.h
+++ b/flang/include/flang/Semantics/scope.h
@@ -260,6 +260,17 @@ class Scope {
 
   void add_importName(const SourceName &);
 
+  // When a module name appears in a PUBLIC or PRIVATE <access-stmt>, all
+  // entities accessed from that module inherit the accessibility attribute.
+  std::optional<Attr> GetModuleAccessibility(const Symbol &module) const {
+    auto it = useModuleAccessibility_.find(&module);
+    return it != useModuleAccessibility_.end() ? std::optional{it->second}
+                                               : std::nullopt;
+  }
+  bool SetModuleAccessibility(const Symbol &module, Attr access) {
+    return useModuleAccessibility_.emplace(&module, access).second;
+  }
+
   // These members pertain to instantiations of parameterized derived types.
   const DerivedTypeSpec *derivedTypeSpec() const { return derivedTypeSpec_; }
   DerivedTypeSpec *derivedTypeSpec() { return derivedTypeSpec_; }
@@ -330,6 +341,9 @@ class Scope {
   const DeclTypeSpec &MakeLengthlessType(DeclTypeSpec &&);
 
   friend llvm::raw_ostream &operator<<(llvm::raw_ostream &, const Scope &);
+
+  // <access-stmt> Map for explicit PUBLIC/PRIVATE module USE
+  std::map<const Symbol *, Attr> useModuleAccessibility_;
 };
 
 // Inline so that it can be called from Evaluate without a link-time dependency.
diff --git a/flang/lib/Semantics/resolve-names.cpp b/flang/lib/Semantics/resolve-names.cpp
index 839f8edeceb97..0f4fd78a92b46 100644
--- a/flang/lib/Semantics/resolve-names.cpp
+++ b/flang/lib/Semantics/resolve-names.cpp
@@ -846,6 +846,14 @@ class ModuleVisitor : public virtual ScopeHandler {
   std::set<SourceName> intrinsicUses_;
   std::set<SourceName> nonIntrinsicUses_;
 
+  void ApplyModuleAccessibility(Symbol &symbol) {
+    if (useModuleScope_ && useModuleScope_->symbol()) {
+      if (auto accessibility{
+              currScope().GetModuleAccessibility(*useModuleScope_->symbol())}) {
+        symbol.attrs().set(*accessibility);
+      }
+    }
+  }
   Symbol &SetAccess(const SourceName &, Attr attr, Symbol * = nullptr);
   // A rename in a USE statement: local => use
   struct SymbolRename {
@@ -4081,6 +4089,7 @@ void ModuleVisitor::DoAddUse(SourceName location, SourceName localName,
       localSymbol->implicitAttrs() =
           localSymbol->attrs() & Attrs{Attr::ASYNCHRONOUS, Attr::VOLATILE};
       localSymbol->flags() = useSymbol.flags();
+      ApplyModuleAccessibility(*localSymbol);
       return;
     }
   }
@@ -4323,6 +4332,8 @@ void ModuleVisitor::DoAddUse(SourceName location, SourceName localName,
           useUltimate.attrs() & ~Attrs{Attr::PUBLIC, Attr::PRIVATE},
           UseDetails{localName, useUltimate})};
       newSymbol.flags() = useSymbol.flags();
+      // Apply module accessibility if specified
+      ApplyModuleAccessibility(newSymbol);
       return;
     } else {
       for (const auto &ref : useGeneric->specificProcs()) {
@@ -4379,6 +4390,8 @@ void ModuleVisitor::DoAddUse(SourceName location, SourceName localName,
       localSymbol->attrs() =
           useSymbol.attrs() & ~Attrs{Attr::PUBLIC, Attr::PRIVATE};
       localSymbol->flags() = useSymbol.flags();
+      // Apply module accessibility if specified
+      ApplyModuleAccessibility(*localSymbol);
       AddGenericUse(*localGeneric, localName, useUltimate);
       // Don't duplicate specific procedures.
       std::size_t originalLocalSpecifics{localGeneric->specificProcs().size()};
@@ -4421,6 +4434,7 @@ void ModuleVisitor::DoAddUse(SourceName location, SourceName localName,
         useUltimate.attrs() & ~Attrs{Attr::PUBLIC, Attr::PRIVATE},
         std::move(generic))};
     newSymbol.flags() = useUltimate.flags();
+    ApplyModuleAccessibility(newSymbol);
     auto &newUseGeneric{newSymbol.get<GenericDetails>()};
     AddGenericUse(newUseGeneric, localName, useUltimate);
     newUseGeneric.AddUse(*localSymbol);
@@ -9810,6 +9824,41 @@ bool ModuleVisitor::Pre(const parser::AccessStmt &x) {
     for (const auto &accessId : accessIds) {
       GenericSpecInfo info{accessId.v.value()};
       auto *symbol{FindInScope(info.symbolName())};
+      // An access-spec may have an access-name (module) in order to set
+      // the default accessibility for everything USE-associated from
+      // that module. The module name won't be in the current scope,
+      // so we need to check the global scope.
+      if (info.kind().IsName()) {
+        const Symbol *moduleSymbol{nullptr};
+        Symbol *resolveSymbol{nullptr};
+        if (symbol) {
+          // Check if symbol in current scope is USE-associated from a module
+          const Symbol &ultimate{symbol->GetUltimate()};
+          if (ultimate.has<ModuleDetails>()) {
+            moduleSymbol = &ultimate;
+            resolveSymbol = symbol;
+          }
+        } else {
+          // Look for a module with this name in the global scope
+          if (auto it{context().globalScope().find(info.symbolName())};
+              it != context().globalScope().end()) {
+            Symbol &globalSymbol{*it->second};
+            if (globalSymbol.has<ModuleDetails>()) {
+              moduleSymbol = &globalSymbol;
+              resolveSymbol = &globalSymbol;
+            }
+          }
+        }
+        if (moduleSymbol) {
+          if (!currScope().SetModuleAccessibility(*moduleSymbol, accessAttr)) {
+            Say(info.symbolName(),
+                "The accessibility of entities from module '%s' has already been specified"_err_en_US,
+                moduleSymbol->name());
+          }
+          info.Resolve(resolveSymbol);
+          continue;
+        }
+      }
       if (!symbol && !info.kind().IsName()) {
         symbol = &MakeSymbol(info.symbolName(), Attrs{}, GenericDetails{});
       }
@@ -10139,6 +10188,29 @@ void ResolveNamesVisitor::FinishSpecificationPart(
   if (inInterfaceBlock()) {
     FinishNamelists(); // NAMELIST is useless in an interface, but allowed
   }
+
+  // Apply deferred module accessibility: F2023: Clause 8.6.1
+  // An <access-spec> with an <access-id> of a module name sets the
+  // accessibility for all entities USE-associated from that module.
+  for (auto &pair : currScope()) {
+    auto &symbol{*pair.second};
+    if (const auto *useDetails{symbol.detailsIf<UseDetails>()}) {
+      // If symbol already has explicit accessibility, skip it
+      if (symbol.attrs().HasAny({Attr::PUBLIC, Attr::PRIVATE})) {
+        continue;
+      }
+      // Find the module that this symbol came from
+      const Symbol &useSymbol{useDetails->symbol()};
+      const Scope *moduleScope{FindModuleContaining(useSymbol.owner())};
+      if (moduleScope && moduleScope->symbol()) {
+        if (auto accessibility{
+                currScope().GetModuleAccessibility(*moduleScope->symbol())}) {
+          symbol.attrs().set(*accessibility);
+        }
+      }
+    }
+  }
+
   for (auto &pair : currScope()) {
     auto &symbol{*pair.second};
     if (inInterfaceBlock()) {
diff --git a/flang/test/Semantics/modfile-accessibility01.f90 b/flang/test/Semantics/modfile-accessibility01.f90
new file mode 100644
index 0000000000000..ae7b0fba62701
--- /dev/null
+++ b/flang/test/Semantics/modfile-accessibility01.f90
@@ -0,0 +1,27 @@
+! RUN: %python %S/test_errors.py %s %flang_fc1
+! Test that entities from a module with PRIVATE modulename are not accessible from further USE
+module basemod
+  implicit none
+  type :: base_type
+    integer :: x
+  end type
+  integer :: base_var = 42
+contains
+  subroutine base_sub()
+  end subroutine
+end module
+
+module middlemod
+  use basemod
+  implicit none
+  private basemod  ! Make all entities from basemod private
+  integer :: middle_var = 100
+end module
+
+program main
+  ! ERROR: 'base_var' is PRIVATE in 'middlemod'
+  use middlemod, only: base_var
+  ! ERROR: 'base_sub' is PRIVATE in 'middlemod'
+  use middlemod, only: base_sub
+  implicit none
+end
diff --git a/flang/test/Semantics/modfile-accessibility02.f90 b/flang/test/Semantics/modfile-accessibility02.f90
new file mode 100644
index 0000000000000..8361fd9434a29
--- /dev/null
+++ b/flang/test/Semantics/modfile-accessibility02.f90
@@ -0,0 +1,26 @@
+! RUN: %flang_fc1 -fsyntax-only %s
+! Test that entities from a module with PUBLIC modulename remain accessible
+module basemod
+  implicit none
+  private  !-- Default is private
+  integer, public :: base_public_var = 42
+  integer         :: base_private_var = 99
+end module
+
+module middlemod
+  use basemod
+  implicit none
+  private         !-- Default is private
+  public basemod  !-- But entities from basemod should remain public
+  integer :: middle_private_var = 100
+end module
+
+program main
+  use middlemod
+  implicit none
+  integer :: x
+  ! base_public_var should be accessible because of "public basemod"
+  x = base_public_var
+  ! This should compile without errors
+  print *, x
+end program
diff --git a/flang/test/Semantics/modfile-accessibility03.f90 b/flang/test/Semantics/modfile-accessibility03.f90
new file mode 100644
index 0000000000000..b35763cadf356
--- /dev/null
+++ b/flang/test/Semantics/modfile-accessibility03.f90
@@ -0,0 +1,14 @@
+! RUN: %python %S/test_errors.py %s %flang_fc1
+! Test error when same module is given conflicting accessibility
+module basemod
+  implicit none
+  integer :: base_var = 42
+end module
+
+module testmod
+  use basemod
+  implicit none
+  private basemod
+  !ERROR: The accessibility of entities from module 'basemod' has already been specified
+  public basemod
+end module
diff --git a/flang/test/Semantics/modfile-accessibility04.f90 b/flang/test/Semantics/modfile-accessibility04.f90
new file mode 100644
index 0000000000000..4fc02156b9a00
--- /dev/null
+++ b/flang/test/Semantics/modfile-accessibility04.f90
@@ -0,0 +1,25 @@
+! RUN: %python %S/test_errors.py %s %flang_fc1
+! Test that explicit PUBLIC/PRIVATE on individual symbols overrides module-level accessibility
+module basemod
+  implicit none
+  integer :: var1 = 1
+  integer :: var2 = 2
+  integer :: var3 = 3
+end module
+
+module middlemod
+  use basemod
+  implicit none
+  private basemod  ! Make all entities from basemod private by default
+  public :: var2   ! But explicitly make var2 public
+end module
+
+program main
+  ! var2 should be accessible because of explicit "public :: var2"
+  use middlemod, only: var2
+  ! ERROR: 'var1' is PRIVATE in 'middlemod'
+  use middlemod, only: var1
+  ! ERROR: 'var3' is PRIVATE in 'middlemod'
+  use middlemod, only: var3
+  implicit none
+end

>From 1f168dd433f837b5ad3c7efbd3d145f480d8d134 Mon Sep 17 00:00:00 2001
From: Ted Johnson <tedmjohnson at protonmail.com>
Date: Tue, 5 May 2026 11:43:59 -0500
Subject: [PATCH 2/2] [flang] Pre-scan approach, without Scope-level state

Replace Symbol-keyed scope storage with a SourceName-keyed map in
SpecificationPartState. Eliminate FinishSpecificationPart pass
with a pre-scan of ACCESS statements before the USE walk. Use a
cleaner implementation for processing PUBLIC/PRIVATE updates.

Add two new tests for re-naming and re-exporting.

Co-authored-by: Kevin Wyatt <kwyatt at hpe.com>
---
 flang/include/flang/Semantics/scope.h         |  14 --
 flang/lib/Semantics/resolve-names.cpp         | 168 +++++++++++++-----
 .../Semantics/modfile-accessibility03.f90     |   2 +-
 .../Semantics/modfile-accessibility05.f90     | 103 +++++++++++
 .../Semantics/modfile-accessibility06.f90     | 151 ++++++++++++++++
 5 files changed, 374 insertions(+), 64 deletions(-)
 create mode 100644 flang/test/Semantics/modfile-accessibility05.f90
 create mode 100644 flang/test/Semantics/modfile-accessibility06.f90

diff --git a/flang/include/flang/Semantics/scope.h b/flang/include/flang/Semantics/scope.h
index c48cde86cfef2..16553fe692e79 100644
--- a/flang/include/flang/Semantics/scope.h
+++ b/flang/include/flang/Semantics/scope.h
@@ -260,17 +260,6 @@ class Scope {
 
   void add_importName(const SourceName &);
 
-  // When a module name appears in a PUBLIC or PRIVATE <access-stmt>, all
-  // entities accessed from that module inherit the accessibility attribute.
-  std::optional<Attr> GetModuleAccessibility(const Symbol &module) const {
-    auto it = useModuleAccessibility_.find(&module);
-    return it != useModuleAccessibility_.end() ? std::optional{it->second}
-                                               : std::nullopt;
-  }
-  bool SetModuleAccessibility(const Symbol &module, Attr access) {
-    return useModuleAccessibility_.emplace(&module, access).second;
-  }
-
   // These members pertain to instantiations of parameterized derived types.
   const DerivedTypeSpec *derivedTypeSpec() const { return derivedTypeSpec_; }
   DerivedTypeSpec *derivedTypeSpec() { return derivedTypeSpec_; }
@@ -341,9 +330,6 @@ class Scope {
   const DeclTypeSpec &MakeLengthlessType(DeclTypeSpec &&);
 
   friend llvm::raw_ostream &operator<<(llvm::raw_ostream &, const Scope &);
-
-  // <access-stmt> Map for explicit PUBLIC/PRIVATE module USE
-  std::map<const Symbol *, Attr> useModuleAccessibility_;
 };
 
 // Inline so that it can be called from Evaluate without a link-time dependency.
diff --git a/flang/lib/Semantics/resolve-names.cpp b/flang/lib/Semantics/resolve-names.cpp
index 0f4fd78a92b46..56a791daf56d9 100644
--- a/flang/lib/Semantics/resolve-names.cpp
+++ b/flang/lib/Semantics/resolve-names.cpp
@@ -770,6 +770,13 @@ class ScopeHandler : public ImplicitRulesVisitor {
       std::set<SourceName> entities; // names of entities with save attr
       std::set<SourceName> commons; // names of common blocks with save attr
     } saveInfo;
+    // F2023 8.6.1: module-name access-ids from PUBLIC/PRIVATE statements;
+    std::map<SourceName, Attr> moduleAccessibility;
+    // F2023 8.6.1: source positions of duplicate module-name access-ids
+    std::set<const char *> duplicateModuleAccessibility;
+    // F2023 8.6.1: for each USE-associated local symbol name, the set of
+    // module names that contributed it via USE association.
+    std::map<SourceName, std::set<SourceName>> symbolUseModules;
   } specPartState_;
 
   // Some declaration processing can and should be deferred to
@@ -825,6 +832,8 @@ class ModuleVisitor : public virtual ScopeHandler {
   Symbol &AddGenericUse(GenericDetails &, const SourceName &, const Symbol &);
   void AddAndCheckModuleUse(SourceName, bool isIntrinsic);
   void CollectUseRenames(const parser::UseStmt &);
+  void CollectModuleAccessibility(
+      const std::list<parser::DeclarationConstruct> &);
   void ClearUseRenames() { useRenames_.clear(); }
   void ClearUseOnly() { useOnly_.clear(); }
   void ClearModuleUses() {
@@ -847,11 +856,12 @@ class ModuleVisitor : public virtual ScopeHandler {
   std::set<SourceName> nonIntrinsicUses_;
 
   void ApplyModuleAccessibility(Symbol &symbol) {
+    // F2023 8.6.1: Record that useModuleScope_ contributed this symbol.
+    // The actual accessibility is computed in FinishSpecificationPart()
+    // once all contributing modules are known.
     if (useModuleScope_ && useModuleScope_->symbol()) {
-      if (auto accessibility{
-              currScope().GetModuleAccessibility(*useModuleScope_->symbol())}) {
-        symbol.attrs().set(*accessibility);
-      }
+      specPartState_.symbolUseModules[symbol.name()].insert(
+          useModuleScope_->symbol()->name());
     }
   }
   Symbol &SetAccess(const SourceName &, Attr attr, Symbol * = nullptr);
@@ -3786,6 +3796,47 @@ void ModuleVisitor::CollectUseRenames(const parser::UseStmt &useStmt) {
       useStmt.u);
 }
 
+// F2023 8.6.1: Pre-scan PUBLIC/PRIVATE statements for plain name access-ids,
+// recording (name, attr) into specPartState_.moduleAccessibility before USE
+// statements are walked.  The scan is purely syntactic - all Name access-ids
+// are inserted regardless of whether they name modules or local entities.
+void ModuleVisitor::CollectModuleAccessibility(
+    const std::list<parser::DeclarationConstruct> &decls) {
+  auto &map{specPartState_.moduleAccessibility};
+  for (const auto &decl : decls) {
+    const auto *specConst{std::get_if<parser::SpecificationConstruct>(&decl.u)};
+    if (!specConst) {
+      continue;
+    }
+    const auto *otherStmt{
+        std::get_if<parser::Statement<parser::OtherSpecificationStmt>>(
+            &specConst->u)};
+    if (!otherStmt) {
+      continue;
+    }
+    const auto *accessInd{std::get_if<common::Indirection<parser::AccessStmt>>(
+        &otherStmt->statement.u)};
+    if (!accessInd) {
+      continue;
+    }
+    const auto &[accessSpec, accessIds] = accessInd->value().t;
+    Attr attr{accessSpec.v == parser::AccessSpec::Kind::Private ? Attr::PRIVATE
+                                                                : Attr::PUBLIC};
+    for (const auto &accessId : accessIds) {
+      const auto &spec{accessId.v.value()};
+      const auto *name{std::get_if<parser::Name>(&spec.u)};
+      if (!name) {
+        continue;
+      }
+      auto [it, inserted]{map.emplace(name->source, attr)};
+      if (!inserted) {
+        specPartState_.duplicateModuleAccessibility.insert(
+            name->source.begin());
+      }
+    }
+  }
+}
+
 bool ModuleVisitor::Pre(const parser::Rename::Names &x) {
   const auto &localName{std::get<0>(x.t)};
   const auto &useName{std::get<1>(x.t)};
@@ -4096,7 +4147,9 @@ void ModuleVisitor::DoAddUse(SourceName location, SourceName localName,
 
   Symbol &localUltimate{localSymbol->GetUltimate()};
   if (&localUltimate == &useUltimate) {
-    // use-associating the same symbol again -- ok
+    // use-associating the same symbol again -- ok, but record the
+    // contributing module for multi-source accessibility (F2023 8.6.1).
+    ApplyModuleAccessibility(*localSymbol);
     return;
   }
   if (useUltimate.owner().IsModule() && localUltimate.owner().IsSubmodule() &&
@@ -9824,36 +9877,30 @@ bool ModuleVisitor::Pre(const parser::AccessStmt &x) {
     for (const auto &accessId : accessIds) {
       GenericSpecInfo info{accessId.v.value()};
       auto *symbol{FindInScope(info.symbolName())};
-      // An access-spec may have an access-name (module) in order to set
-      // the default accessibility for everything USE-associated from
-      // that module. The module name won't be in the current scope,
-      // so we need to check the global scope.
+      // F2023 8.6.1: a module-name access-id sets default accessibility for
+      // entities USE associated from that module.  The (name, attr) pair was
+      // already recorded by CollectModuleAccessibility(); here we just verify
+      // that it resolves to a module, and skip the per-entity SetAccess path.
       if (info.kind().IsName()) {
         const Symbol *moduleSymbol{nullptr};
         Symbol *resolveSymbol{nullptr};
-        if (symbol) {
-          // Check if symbol in current scope is USE-associated from a module
-          const Symbol &ultimate{symbol->GetUltimate()};
-          if (ultimate.has<ModuleDetails>()) {
-            moduleSymbol = &ultimate;
-            resolveSymbol = symbol;
-          }
-        } else {
-          // Look for a module with this name in the global scope
+        if (!symbol) {
           if (auto it{context().globalScope().find(info.symbolName())};
-              it != context().globalScope().end()) {
-            Symbol &globalSymbol{*it->second};
-            if (globalSymbol.has<ModuleDetails>()) {
-              moduleSymbol = &globalSymbol;
-              resolveSymbol = &globalSymbol;
-            }
+              it != context().globalScope().end() &&
+              it->second->has<ModuleDetails>()) {
+            moduleSymbol = &*it->second;
+            resolveSymbol = &*it->second;
           }
+        } else if (symbol->GetUltimate().has<ModuleDetails>()) {
+          moduleSymbol = &symbol->GetUltimate();
+          resolveSymbol = symbol;
         }
         if (moduleSymbol) {
-          if (!currScope().SetModuleAccessibility(*moduleSymbol, accessAttr)) {
+          if (specPartState_.duplicateModuleAccessibility.count(
+                  info.symbolName().begin())) {
             Say(info.symbolName(),
-                "The accessibility of entities from module '%s' has already been specified"_err_en_US,
-                moduleSymbol->name());
+                "The name of module '%s' shall appear at most once in all of the ACCESS statements in a module"_err_en_US,
+                moduleSymbol->name()); // F2023 8.6.1: (C876)
           }
           info.Resolve(resolveSymbol);
           continue;
@@ -9977,6 +10024,11 @@ bool ResolveNamesVisitor::Pre(const parser::SpecificationPart &x) {
   Walk(accDecls);
   Walk(ompDecls);
   Walk(compilerDirectives);
+  // F2023 8.6.1: collect module-name access-ids before USE walk so that
+  // DoAddUse() can record contributing modules regardless of statement order.
+  if (currScope().IsModule()) {
+    CollectModuleAccessibility(decls);
+  }
   for (const auto &useStmt : useStmts) {
     CollectUseRenames(useStmt.statement.value());
   }
@@ -10183,34 +10235,52 @@ void ResolveNamesVisitor::CreateGeneric(const parser::GenericSpec &x) {
 void ResolveNamesVisitor::FinishSpecificationPart(
     const std::list<parser::DeclarationConstruct> &decls) {
   misparsedStmtFuncFound_ = false;
-  funcResultStack().CompleteFunctionResultType();
-  CheckImports();
-  if (inInterfaceBlock()) {
-    FinishNamelists(); // NAMELIST is useless in an interface, but allowed
-  }
-
-  // Apply deferred module accessibility: F2023: Clause 8.6.1
-  // An <access-spec> with an <access-id> of a module name sets the
-  // accessibility for all entities USE-associated from that module.
-  for (auto &pair : currScope()) {
-    auto &symbol{*pair.second};
-    if (const auto *useDetails{symbol.detailsIf<UseDetails>()}) {
-      // If symbol already has explicit accessibility, skip it
-      if (symbol.attrs().HasAny({Attr::PUBLIC, Attr::PRIVATE})) {
+  // F2023 8.6.1: apply module-name accessibility to USE associated symbols
+  // now that all contributing modules are known.  Skip symbols which already
+  // carry PUBLIC or PRIVATE (from per-entity access-ids).  The rule
+  // applies only when every contributing module is named in an access-stmt:
+  //   * any PUBLIC => PUBLIC
+  //   * all in map && all PRIVATE => PRIVATE
+  //   * not all in map => skip (leave unset)
+  if (!specPartState_.symbolUseModules.empty() &&
+      !specPartState_.moduleAccessibility.empty()) {
+    const auto &accessMap{specPartState_.moduleAccessibility};
+    for (auto &[symName, symbolRef] : currScope()) {
+      Symbol &symbol{*symbolRef};
+      auto symIter{specPartState_.symbolUseModules.find(symbol.name())};
+      if (symIter == specPartState_.symbolUseModules.end()) {
         continue;
       }
-      // Find the module that this symbol came from
-      const Symbol &useSymbol{useDetails->symbol()};
-      const Scope *moduleScope{FindModuleContaining(useSymbol.owner())};
-      if (moduleScope && moduleScope->symbol()) {
-        if (auto accessibility{
-                currScope().GetModuleAccessibility(*moduleScope->symbol())}) {
-          symbol.attrs().set(*accessibility);
+      if (symbol.attrs().HasAny({Attr::PUBLIC, Attr::PRIVATE})) {
+        continue; // per-entity access-id already applied
+      }
+      // F2023 8.6.1: the module-name accessibility rule applies only when
+      // "the name of every module from which it is accessed appears in an
+      // access-stmt in the scoping unit".
+      bool allInMap{true}, anyPublic{false};
+      for (const SourceName &moduleName : symIter->second) {
+        auto modIter{accessMap.find(moduleName)};
+        if (modIter == accessMap.end()) {
+          allInMap = false;
+          break;
+        }
+        if (modIter->second == Attr::PUBLIC) {
+          anyPublic = true;
         }
       }
+      if (!allInMap) {
+        continue; // not every contributing module named in access-stmts
+      }
+      // All contributing modules are named: any PUBLIC => PUBLIC, else all
+      // are PRIVATE => PRIVATE.
+      symbol.attrs().set(anyPublic ? Attr::PUBLIC : Attr::PRIVATE);
     }
   }
-
+  funcResultStack().CompleteFunctionResultType();
+  CheckImports();
+  if (inInterfaceBlock()) {
+    FinishNamelists(); // NAMELIST is useless in an interface, but allowed
+  }
   for (auto &pair : currScope()) {
     auto &symbol{*pair.second};
     if (inInterfaceBlock()) {
diff --git a/flang/test/Semantics/modfile-accessibility03.f90 b/flang/test/Semantics/modfile-accessibility03.f90
index b35763cadf356..82f4a51caecd1 100644
--- a/flang/test/Semantics/modfile-accessibility03.f90
+++ b/flang/test/Semantics/modfile-accessibility03.f90
@@ -9,6 +9,6 @@ module testmod
   use basemod
   implicit none
   private basemod
-  !ERROR: The accessibility of entities from module 'basemod' has already been specified
+  !ERROR: The name of module 'basemod' shall appear at most once in all of the ACCESS statements in a module
   public basemod
 end module
diff --git a/flang/test/Semantics/modfile-accessibility05.f90 b/flang/test/Semantics/modfile-accessibility05.f90
new file mode 100644
index 0000000000000..6c00cb7a434a9
--- /dev/null
+++ b/flang/test/Semantics/modfile-accessibility05.f90
@@ -0,0 +1,103 @@
+! RUN: %python %S/test_errors.py %s %flang_fc1
+! Tests F2023 8.6.1 p2: module-name access-ids when the same entity is
+! accessible via more than one USE-associated module.
+!   - all source modules PRIVATE -> entity is PRIVATE
+!   - any source module PUBLIC   -> entity is PUBLIC (PUBLIC wins)
+!   - not all source modules named in access-stmts -> module default applies
+module base
+  implicit none
+  integer :: x = 10
+  integer :: y = 20
+end module
+
+! Both re-export x and y from base (default PUBLIC).
+module re_a
+  use base
+end module
+
+module re_b
+  use base
+end module
+
+! Both source modules marked PRIVATE: x and y are PRIVATE.
+module both_private
+  use re_a
+  use re_b
+  implicit none
+  private re_a
+  private re_b
+end module
+
+! One source module PRIVATE, one PUBLIC: PUBLIC wins; x and y are PUBLIC.
+module mixed_access
+  use re_a
+  use re_b
+  implicit none
+  private re_a
+  public re_b
+end module
+
+! Only re_a has an access-stmt; re_b does not.
+! F2023 8.6.1 requires ALL source modules to be named for the rule to apply;
+! x and y fall back to the module default (PUBLIC).
+module partial_stmt
+  use re_a
+  use re_b
+  implicit none
+  private re_a
+end module
+
+! Case 4: merged generics from two modules with mixed access-stmts.
+! The generic interface 'op' should be PUBLIC (because gen_re_b is PUBLIC).
+module gen_base
+  implicit none
+  interface op
+    module procedure op_int
+  end interface
+contains
+  integer function op_int(a)
+    integer, intent(in) :: a
+    op_int = a
+  end function
+end module
+
+module gen_re_a
+  use gen_base
+end module
+
+module gen_re_b
+  use gen_base
+end module
+
+module gen_mixed
+  use gen_re_a
+  use gen_re_b
+  implicit none
+  private gen_re_a
+  public gen_re_b
+end module
+
+program test
+  implicit none
+  ! Case 1: both source modules PRIVATE -> entities are PRIVATE.
+  block
+    ! ERROR: 'x' is PRIVATE in 'both_private'
+    use both_private, only: x
+  end block
+  block
+    ! ERROR: 'y' is PRIVATE in 'both_private'
+    use both_private, only: y
+  end block
+  ! Case 2: PUBLIC wins over PRIVATE -> x and y are accessible (no error).
+  block
+    use mixed_access, only: x, y
+  end block
+  ! Case 3: partial access-stmt -> module default PUBLIC applies (no error).
+  block
+    use partial_stmt, only: x, y
+  end block
+  ! Case 4: merged generic PUBLIC via gen_re_b -> accessible (no error).
+  block
+    use gen_mixed, only: op
+  end block
+end program
diff --git a/flang/test/Semantics/modfile-accessibility06.f90 b/flang/test/Semantics/modfile-accessibility06.f90
new file mode 100644
index 0000000000000..8b49ed3f92a31
--- /dev/null
+++ b/flang/test/Semantics/modfile-accessibility06.f90
@@ -0,0 +1,151 @@
+! RUN: %python %S/test_errors.py %s %flang_fc1
+! Tests F2023 8.6.1 p2: module-name accessibility with USE renames.
+! Exercises rename at the re-export layer and at the consuming USE site.
+
+module origin
+  implicit none
+  integer :: velocity = 100
+  integer :: altitude = 200
+end module
+
+! relay_alpha re-exports origin's symbols under their original names.
+module relay_alpha
+  use origin
+end module
+
+! relay_beta re-exports origin::velocity under the name 'speed'.
+module relay_beta
+  use origin, only: speed => velocity
+end module
+
+! relay_gamma re-exports origin's symbols under their original names
+! (same as relay_alpha, for multi-source tests).
+module relay_gamma
+  use origin
+end module
+
+! Case 1: Same ultimate via rename -- both PRIVATE.
+! relay_alpha exports 'velocity', relay_beta exports 'speed'.
+! These are different local names, so no collision; each gets its own
+! module's accessibility independently.
+module rename_both_private
+  use relay_alpha, only: velocity
+  use relay_beta, only: speed
+  implicit none
+  private relay_alpha
+  private relay_beta
+end module
+
+! Case 2: Same ultimate via rename -- mixed access on separate names.
+! velocity comes from relay_alpha (PRIVATE), speed comes from relay_beta
+! (PUBLIC).  No collision; each reflects its source module's access.
+module rename_mixed_separate
+  use relay_alpha, only: velocity
+  use relay_beta, only: speed
+  implicit none
+  private relay_alpha
+  public relay_beta
+end module
+
+! Case 3: Consumer-side rename merges two sources under one local name.
+! Both relay_alpha::velocity and relay_beta::speed have the same ultimate
+! (origin::velocity).  The consumer renames speed back to 'velocity',
+! so both contribute to local name 'velocity'.
+! relay_alpha is PRIVATE, relay_gamma is PUBLIC -> PUBLIC wins.
+module rename_consumer_merge
+  use relay_alpha, only: velocity
+  use relay_gamma, only: velocity
+  implicit none
+  private relay_alpha
+  public relay_gamma
+end module
+
+! Case 4: Consumer-side rename merges two sources, both PRIVATE.
+module rename_consumer_both_private
+  use relay_alpha, only: velocity
+  use relay_gamma, only: velocity
+  implicit none
+  private relay_alpha
+  private relay_gamma
+end module
+
+! Case 5: Consumer-side rename with partial access-stmt.
+! relay_alpha is PRIVATE, relay_gamma has no access-stmt.
+! Not all contributing modules are named -> default PUBLIC applies.
+module rename_consumer_partial
+  use relay_alpha, only: velocity
+  use relay_gamma, only: velocity
+  implicit none
+  private relay_alpha
+end module
+
+! Case 6: Re-export rename brought into consumer under yet another name.
+! relay_beta exports 'speed' (renamed from origin::velocity).
+! Consumer renames 'speed' to 'rate'.  Both PRIVATE -> PRIVATE.
+module rename_chain_private
+  use relay_alpha, only: rate => velocity
+  use relay_beta, only: rate => speed
+  implicit none
+  private relay_alpha
+  private relay_beta
+end module
+
+! Case 7: Re-export rename chain, mixed access -> PUBLIC wins.
+module rename_chain_mixed
+  use relay_alpha, only: rate => velocity
+  use relay_beta, only: rate => speed
+  implicit none
+  private relay_alpha
+  public relay_beta
+end module
+
+program test_renames
+  implicit none
+
+  ! Case 1: separate names, both PRIVATE.
+  block
+    !ERROR: 'velocity' is PRIVATE in 'rename_both_private'
+    use rename_both_private, only: velocity
+  end block
+  block
+    !ERROR: 'speed' is PRIVATE in 'rename_both_private'
+    use rename_both_private, only: speed
+  end block
+
+  ! Case 2: separate names, mixed access.
+  ! velocity from relay_alpha (PRIVATE), speed from relay_beta (PUBLIC).
+  block
+    !ERROR: 'velocity' is PRIVATE in 'rename_mixed_separate'
+    use rename_mixed_separate, only: velocity
+  end block
+  block
+    use rename_mixed_separate, only: speed  ! PUBLIC, no error
+  end block
+
+  ! Case 3: consumer merge under one name, PUBLIC wins (no error).
+  block
+    use rename_consumer_merge, only: velocity
+  end block
+
+  ! Case 4: consumer merge under one name, both PRIVATE.
+  block
+    !ERROR: 'velocity' is PRIVATE in 'rename_consumer_both_private'
+    use rename_consumer_both_private, only: velocity
+  end block
+
+  ! Case 5: consumer merge, partial access-stmt -> default PUBLIC (no error).
+  block
+    use rename_consumer_partial, only: velocity
+  end block
+
+  ! Case 6: rename chain, both PRIVATE.
+  block
+    !ERROR: 'rate' is PRIVATE in 'rename_chain_private'
+    use rename_chain_private, only: rate
+  end block
+
+  ! Case 7: rename chain, mixed -> PUBLIC wins (no error).
+  block
+    use rename_chain_mixed, only: rate
+  end block
+end program



More information about the flang-commits mailing list