[flang][mlir] Add flang to mlir lowering for groupprivate (#180934)

This PR implements the Flang frontend lowering for the OpenMP
`groupprivate`
Changes:
- Update genOMP handler for OpenMPGroupprivate in OpenMP.cpp to generate
`omp.groupprivate` MLIR operation.
- Add clause processing for groupprivate variables
- Add test cases for groupprivate lowering

Co-Authored-By: Claude
[noreply@anthropic.com](mailto:noreply@anthropic.com)
diff --git a/flang/include/flang/Lower/OpenMP.h b/flang/include/flang/Lower/OpenMP.h
index 852a120..882ebad 100644
--- a/flang/include/flang/Lower/OpenMP.h
+++ b/flang/include/flang/Lower/OpenMP.h
@@ -80,6 +80,7 @@
 void genOpenMPSymbolProperties(AbstractConverter &converter,
                                const pft::Variable &var);
 
+void genGroupprivateOp(AbstractConverter &, const pft::Variable &);
 void genThreadprivateOp(AbstractConverter &, const pft::Variable &);
 void genDeclareTargetIntGlobal(AbstractConverter &, const pft::Variable &);
 bool isOpenMPTargetConstruct(const parser::OpenMPConstruct &);
diff --git a/flang/include/flang/Semantics/symbol.h b/flang/include/flang/Semantics/symbol.h
index 40c49c5..23c26ba7 100644
--- a/flang/include/flang/Semantics/symbol.h
+++ b/flang/include/flang/Semantics/symbol.h
@@ -69,14 +69,28 @@
   const OmpClauseSet &ompDeclTarget() const { return ompDeclTarget_; }
   void set_ompDeclTarget(OmpClauseSet clauses) { ompDeclTarget_ = clauses; }
 
-  const std::optional<common::OmpDeviceType> &ompDeviceType() const {
-    return ompDeviceType_;
+  const std::optional<common::OmpDeviceType> &ompDeclTargetDeviceType() const {
+    return ompDeclTargetDeviceType_;
   }
   void set_ompDeclTarget(common::OmpDeviceType device) {
-    ompDeviceType_ = device;
+    ompDeclTargetDeviceType_ = device;
   }
 
+  const OmpClauseSet &ompGroupprivate() const { return ompGroupprivate_; }
+  void set_ompGroupprivate(OmpClauseSet clauses) { ompGroupprivate_ = clauses; }
+
+  const std::optional<common::OmpDeviceType> &
+  ompGroupprivateDeviceType() const {
+    return ompGroupprivateDeviceType_;
+  }
+  void set_ompGroupprivate(common::OmpDeviceType device) {
+    ompGroupprivateDeviceType_ = device;
+  }
+
+  // \p dir indicates to which declarative directive the given clauses
+  // belong to.
   void printClauseSet(llvm::raw_ostream &os, const OmpClauseSet &clauses,
+      llvm::omp::Directive dir,
       parser::CharBlock name = parser::CharBlock{}) const;
   friend llvm::raw_ostream &operator<<(
       llvm::raw_ostream &, const WithOmpDeclarative &);
@@ -98,7 +112,12 @@
   OmpClauseSet ompDeclTarget_;
   // The argument to DEVICE_TYPE clause. Only needed when the clause is
   // present in the ompDeclTarget_ set.
-  std::optional<common::OmpDeviceType> ompDeviceType_;
+  std::optional<common::OmpDeviceType> ompDeclTargetDeviceType_;
+  // The set of clauses on a GROUPPRIVATE directive declaring this symbol.
+  OmpClauseSet ompGroupprivate_;
+  // The argument to a DEVICE_TYPE clause on a GROUPPRIVATE directive declaring
+  // this symbol. Only needed when the clause is present in ompGroupprivate_.
+  std::optional<common::OmpDeviceType> ompGroupprivateDeviceType_;
 };
 
 // A module or submodule.
diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index 4b0ec26..facca98 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -53,6 +53,8 @@
 #include "mlir/Support/StateStack.h"
 #include "mlir/Transforms/RegionUtils.h"
 #include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallPtrSet.h"
+#include "llvm/ADT/SmallSet.h"
 
 using namespace Fortran::lower::omp;
 using namespace Fortran::common::openmp;
@@ -783,6 +785,111 @@
   }
 }
 
+// Translate a semantics-layer device_type to the MLIR enum used by
+// omp.groupprivate.
+static mlir::omp::DeclareTargetDeviceType
+toMLIRDeclareTargetDeviceType(Fortran::common::OmpDeviceType deviceType) {
+  switch (deviceType) {
+  case Fortran::common::OmpDeviceType::Any:
+    return mlir::omp::DeclareTargetDeviceType::any;
+  case Fortran::common::OmpDeviceType::Host:
+    return mlir::omp::DeclareTargetDeviceType::host;
+  case Fortran::common::OmpDeviceType::Nohost:
+    return mlir::omp::DeclareTargetDeviceType::nohost;
+  }
+  llvm_unreachable("invalid OmpDeviceType");
+}
+
+static void groupprivatizeVars(lower::AbstractConverter &converter,
+                               lower::pft::Evaluation &eval) {
+  fir::FirOpBuilder &firOpBuilder = converter.getFirOpBuilder();
+  mlir::Location currentLocation = converter.getCurrentLocation();
+  mlir::OpBuilder::InsertionGuard guard(firOpBuilder);
+  firOpBuilder.setInsertionPointToStart(firOpBuilder.getAllocaBlock());
+
+  auto module = converter.getModuleOp();
+
+  // Create a groupprivate operation for the symbol.
+  auto genGroupprivateOp = [&](const semantics::Symbol &sym) -> mlir::Value {
+    std::string globalName = converter.mangleName(sym);
+    fir::GlobalOp global = module.lookupSymbol<fir::GlobalOp>(globalName);
+    if (!global)
+      return mlir::Value();
+
+    // The device_type modifier was recorded on the symbol during semantic
+    // analysis.
+    mlir::omp::DeclareTargetDeviceType deviceTypeEnum =
+        mlir::omp::DeclareTargetDeviceType::any;
+    Fortran::common::visit(
+        [&](auto &&details) {
+          using TypeD = llvm::remove_cvref_t<decltype(details)>;
+          if constexpr (std::is_base_of_v<semantics::WithOmpDeclarative,
+                                          TypeD>) {
+            if (auto dt = details.ompGroupprivateDeviceType())
+              deviceTypeEnum = toMLIRDeclareTargetDeviceType(*dt);
+          }
+        },
+        sym.GetUltimate().details());
+    mlir::omp::DeclareTargetDeviceTypeAttr deviceTypeAttr =
+        mlir::omp::DeclareTargetDeviceTypeAttr::get(firOpBuilder.getContext(),
+                                                    deviceTypeEnum);
+
+    // omp.groupprivate takes a flat symbol reference and returns
+    // the address of the per-contention group copy of the global variable.
+    return mlir::omp::GroupprivateOp::create(
+        firOpBuilder, currentLocation, global.resultType(), global.getSymbol(),
+        deviceTypeAttr);
+  };
+
+  llvm::SetVector<const semantics::Symbol *> groupprivateSyms;
+  converter.collectSymbolSet(eval, groupprivateSyms,
+                             semantics::Symbol::Flag::OmpGroupPrivate,
+                             /*collectSymbols=*/true,
+                             /*collectHostAssociatedSymbols=*/true);
+  llvm::SmallSet<semantics::SourceName, 8> groupprivateSymNames;
+
+  // For a COMMON block, the GroupprivateOp is generated for the block itself
+  // instead of its members.
+  llvm::SmallPtrSet<const semantics::Symbol *, 8> commonSyms;
+
+  for (const semantics::Symbol *sym : groupprivateSyms) {
+    mlir::Value symGroupprivateValue;
+    // The variable may be used more than once, and each reference has one
+    // symbol with the same name. Only do once for references of one variable.
+    if (!groupprivateSymNames.insert(sym->name()).second)
+      continue;
+
+    if (const semantics::Symbol *common =
+            semantics::FindCommonBlockContaining(sym->GetUltimate())) {
+      // Handle common block members: create groupprivate op for the entire
+      // common block, then compute member offset.
+      mlir::Value commonGroupprivateValue;
+      if (commonSyms.contains(common)) {
+        commonGroupprivateValue = converter.getSymbolAddress(*common);
+      } else {
+        commonGroupprivateValue = genGroupprivateOp(*common);
+        if (!commonGroupprivateValue)
+          continue;
+        converter.bindSymbol(*common, commonGroupprivateValue);
+        commonSyms.insert(common);
+      }
+      symGroupprivateValue = lower::genCommonBlockMember(
+          converter, currentLocation, sym->GetUltimate(),
+          commonGroupprivateValue, common->size());
+    } else {
+      symGroupprivateValue = genGroupprivateOp(*sym);
+    }
+
+    if (!symGroupprivateValue)
+      continue;
+
+    fir::ExtendedValue sexv = converter.getSymbolExtendedValue(*sym);
+    fir::ExtendedValue symGroupprivateExv =
+        getExtendedValue(sexv, symGroupprivateValue);
+    converter.bindSymbol(*sym, symGroupprivateExv);
+  }
+}
+
 static mlir::Operation *setLoopVar(lower::AbstractConverter &converter,
                                    mlir::Location loc, mlir::Value indexVal,
                                    const semantics::Symbol *sym) {
@@ -1342,6 +1449,10 @@
     }
   }
 
+  // TODO: groupprivate is currently only materialised for `teams` constructs.
+  if (info.dir == llvm::omp::Directive::OMPD_teams)
+    groupprivatizeVars(info.converter, info.eval);
+
   if (!info.genSkeletonOnly) {
     if (ConstructQueue::const_iterator next = std::next(item);
         next != queue.end()) {
@@ -3025,6 +3136,11 @@
         !symbolsWithDynamicSubstring.contains(&sym.GetUltimate()))
       return;
 
+    // Skip groupprivate symbols - they don't need to be mapped because
+    // groupprivate creates its own storage.
+    if (sym.GetUltimate().test(semantics::Symbol::Flag::OmpGroupPrivate))
+      return;
+
     if (!isDuplicateMappedSymbol(sym, dsp.getAllSymbolsToPrivatize(),
                                  hasDeviceAddrObjects, mapObjects,
                                  isDevicePtrObjects)) {
@@ -4623,7 +4739,8 @@
                    semantics::SemanticsContext &semaCtx,
                    lower::pft::Evaluation &eval,
                    const parser::OmpGroupprivateDirective &directive) {
-  TODO(converter.getCurrentLocation(), "GROUPPRIVATE");
+  // The semantic analysis sets the flag and device_type on the
+  // symbols; omp.groupprivate is materialised by groupprivatizeVars.
 }
 
 static void genOMP(lower::AbstractConverter &converter, lower::SymMap &symTable,
@@ -5370,6 +5487,9 @@
   assert(var.hasSymbol() && "Expecting Symbol");
   const semantics::Symbol &sym = var.getSymbol();
 
+  if (sym.test(semantics::Symbol::Flag::OmpGroupPrivate))
+    lower::genGroupprivateOp(converter, var);
+
   if (sym.test(semantics::Symbol::Flag::OmpThreadprivate))
     lower::genThreadprivateOp(converter, var);
 
@@ -5377,6 +5497,31 @@
     lower::genDeclareTargetIntGlobal(converter, var);
 }
 
+void Fortran::lower::genGroupprivateOp(lower::AbstractConverter &converter,
+                                       const lower::pft::Variable &var) {
+  const semantics::Symbol &sym = var.getSymbol();
+
+  // For common block members, the groupprivate op is generated for the entire
+  // common block in groupprivatizeVars, not for individual members here.
+  // The common block already has a global, so nothing to do here.
+  if (semantics::FindCommonBlockContaining(sym.GetUltimate()))
+    return;
+
+  // Handle non-global variables: local variables with the SAVE attribute can
+  // appear in a groupprivate directive. Promote them to fir.global so that
+  // omp.groupprivate can reference them by symbol name.
+  if (!var.isGlobal()) {
+    fir::FirOpBuilder &firOpBuilder = converter.getFirOpBuilder();
+    mlir::Location currentLocation = converter.getCurrentLocation();
+    auto module = converter.getModuleOp();
+    std::string globalName = converter.mangleName(sym);
+    if (!module.lookupSymbol<fir::GlobalOp>(globalName))
+      globalInitialization(converter, firOpBuilder, sym, var, currentLocation);
+  }
+
+  // The actual omp.groupprivate operations are created by groupprivatizeVars.
+}
+
 void Fortran::lower::genThreadprivateOp(lower::AbstractConverter &converter,
                                         const lower::pft::Variable &var) {
   fir::FirOpBuilder &firOpBuilder = converter.getFirOpBuilder();
diff --git a/flang/lib/Semantics/mod-file.cpp b/flang/lib/Semantics/mod-file.cpp
index 2865d16..89a535c 100644
--- a/flang/lib/Semantics/mod-file.cpp
+++ b/flang/lib/Semantics/mod-file.cpp
@@ -387,7 +387,7 @@
       os << "!$omp "
          << parser::ToLowerCaseLetters(llvm::omp::getOpenMPDirectiveName(
                 llvm::omp::Directive::OMPD_requires, version));
-      decls->printClauseSet(os, reqs);
+      decls->printClauseSet(os, reqs, llvm::omp::Directive::OMPD_requires);
       os << "\n";
     }
   }
@@ -405,7 +405,24 @@
            << parser::ToLowerCaseLetters(llvm::omp::getOpenMPDirectiveName(
                   llvm::omp::Directive::OMPD_declare_target, version))
            << " ";
-        decls->printClauseSet(os, dtgt, symbol.name());
+        decls->printClauseSet(
+            os, dtgt, llvm::omp::Directive::OMPD_declare_target, symbol.name());
+        os << "\n";
+      }
+      // Re-emit `!$omp groupprivate` (and its device_type) so a TU that `use`s
+      // this module recovers the directive from the .mod file. Common-block
+      // names must be wrapped in slashes when reparsed.
+      if (const OmpClauseSet &gp{decls->ompGroupprivate()}; gp.count()) {
+        os << "!$omp "
+           << parser::ToLowerCaseLetters(llvm::omp::getOpenMPDirectiveName(
+                  llvm::omp::Directive::OMPD_groupprivate, version))
+           << "(";
+        if (symbol.detailsIf<CommonBlockDetails>())
+          os << '/' << symbol.name() << '/';
+        else
+          os << symbol.name();
+        os << ") ";
+        decls->printClauseSet(os, gp, llvm::omp::Directive::OMPD_groupprivate);
         os << "\n";
       }
     }
diff --git a/flang/lib/Semantics/resolve-directives.cpp b/flang/lib/Semantics/resolve-directives.cpp
index 1e91460..0e2dda8 100644
--- a/flang/lib/Semantics/resolve-directives.cpp
+++ b/flang/lib/Semantics/resolve-directives.cpp
@@ -2190,6 +2190,37 @@
       ResolveOmpObject(*object, Symbol::Flag::OmpGroupPrivate);
     }
   }
+
+  // groupprivate carries an optional device_type clause (OpenMP 6.0). Always
+  // record it, like declare_target's enter clause, so a clause-less directive
+  // still round-trips through symbol dumps and .mod files; an absent clause
+  // means the spec default (any).
+  common::OmpDeviceType device{common::OmpDeviceType::Any};
+  if (auto *devClause{
+          parser::omp::FindClause(x.v, llvm::omp::Clause::OMPC_device_type)}) {
+    device = parser::UnwrapRef<common::OmpDeviceType>(*devClause);
+  }
+
+  unsigned version{context_.langOptions().OpenMPVersion};
+  WithOmpDeclarative::OmpClauseSet clauses;
+  clauses.set(llvm::omp::Clause::OMPC_device_type);
+  for (const parser::OmpArgument &arg : x.v.Arguments().v) {
+    if (const parser::OmpObject *object{parser::omp::GetArgumentObject(arg)}) {
+      if (const Symbol *sym{omp::GetObjectSymbol(*object)}) {
+        common::visit(
+            [&](auto &d) {
+              using TypeD = llvm::remove_cvref_t<decltype(d)>;
+              if constexpr (std::is_base_of_v<WithOmpDeclarative, TypeD>) {
+                d.set_version(version);
+                d.set_ompGroupprivate(clauses);
+                d.set_ompGroupprivate(device);
+              }
+            },
+            const_cast<Symbol &>(sym->GetUltimate()).details());
+      }
+    }
+  }
+
   return true;
 }
 
@@ -2285,7 +2316,7 @@
             if (device) {
               clauseSet.set(llvm::omp::Clause::OMPC_device_type);
               auto &deviceType{
-                  const_cast<decltype(device) &>(d.ompDeviceType())};
+                  const_cast<decltype(device) &>(d.ompDeclTargetDeviceType())};
               deviceType = device;
             }
           } else if constexpr (std::is_same_v<GenericDetails, TypeD> ||
diff --git a/flang/lib/Semantics/symbol.cpp b/flang/lib/Semantics/symbol.cpp
index 546ed3e..3560dac 100644
--- a/flang/lib/Semantics/symbol.cpp
+++ b/flang/lib/Semantics/symbol.cpp
@@ -71,7 +71,8 @@
 }
 
 void WithOmpDeclarative::printClauseSet(llvm::raw_ostream &os,
-    const OmpClauseSet &clauses, parser::CharBlock name) const {
+    const OmpClauseSet &clauses, llvm::omp::Directive dir,
+    parser::CharBlock name) const {
   auto toLower = parser::ToLowerCaseLetters;
 
   size_t idx{0}, size{clauses.count()};
@@ -81,9 +82,16 @@
     case llvm::omp::Clause::OMPC_atomic_default_mem_order:
       os << '(' << toLower(EnumToString(*ompAtomicDefaultMemOrder())) << ')';
       break;
-    case llvm::omp::Clause::OMPC_device_type:
-      os << "(" << toLower(EnumToString(*ompDeviceType())) << ')';
+    case llvm::omp::Clause::OMPC_device_type: {
+      // device_type is carried by several directives; print the value
+      // recorded for the one being emitted.
+      const std::optional<common::OmpDeviceType> &dt{
+          dir == llvm::omp::Directive::OMPD_groupprivate
+              ? ompGroupprivateDeviceType()
+              : ompDeclTargetDeviceType()};
+      os << '(' << toLower(EnumToString(*dt)) << ')';
       break;
+    }
     case llvm::omp::Clause::OMPC_enter:
     case llvm::omp::Clause::OMPC_link:
       if (!name.empty()) {
@@ -108,12 +116,17 @@
 
   if (const OmpClauseSet &reqs{x.ompRequires()}; reqs.count()) {
     os << " OmpRequirements:(";
-    x.printClauseSet(os, reqs);
+    x.printClauseSet(os, reqs, llvm::omp::Directive::OMPD_requires);
     os << ')';
   }
   if (const OmpClauseSet &dtgt{x.ompDeclTarget()}; dtgt.count()) {
     os << " OmpDeclareTargetFlags:(";
-    x.printClauseSet(os, dtgt);
+    x.printClauseSet(os, dtgt, llvm::omp::Directive::OMPD_declare_target);
+    os << ')';
+  }
+  if (const OmpClauseSet &gp{x.ompGroupprivate()}; gp.count()) {
+    os << " OmpGroupprivateFlags:(";
+    x.printClauseSet(os, gp, llvm::omp::Directive::OMPD_groupprivate);
     os << ')';
   }
   return os;
diff --git a/flang/test/Lower/OpenMP/Todo/groupprivate.f90 b/flang/test/Lower/OpenMP/Todo/groupprivate.f90
deleted file mode 100644
index 9ad9b93..0000000
--- a/flang/test/Lower/OpenMP/Todo/groupprivate.f90
+++ /dev/null
@@ -1,9 +0,0 @@
-!RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=60 -o - %s 2>&1 | FileCheck %s
-
-!CHECK: not yet implemented: GROUPPRIVATE
-
-module m
-implicit none
-integer :: x
-!$omp groupprivate(x)
-end module
diff --git a/flang/test/Lower/OpenMP/groupprivate-modfile.f90 b/flang/test/Lower/OpenMP/groupprivate-modfile.f90
new file mode 100644
index 0000000..cc30d7c
--- /dev/null
+++ b/flang/test/Lower/OpenMP/groupprivate-modfile.f90
@@ -0,0 +1,36 @@
+! Cross-TU propagation of `groupprivate` and its device_type via .mod files.
+
+! RUN: rm -rf %t && split-file %s %t
+! RUN: %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=60 -module-dir %t %t/m.f90 -o - > /dev/null
+! RUN: %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=60 -J %t %t/use.f90 -o - | FileCheck %s
+
+! First RUN builds gp_mod.mod.
+! Second RUN lowers a consumer that only USE-associates the module
+! and checks that the omp.groupprivate op picks up device_type recovered
+! from the .mod file rather than the original source.
+
+!--- m.f90
+module gp_mod
+  implicit none
+  integer, save :: gp_x
+  !$omp groupprivate(gp_x) device_type(host)
+end module gp_mod
+
+!--- use.f90
+program use_gp_mod
+  use gp_mod
+  implicit none
+  !$omp teams
+    gp_x = 42
+  !$omp end teams
+end program use_gp_mod
+
+! CHECK-LABEL: func.func @_QQmain
+! CHECK:         fir.address_of(@_QMgp_modEgp_x) : !fir.ref<i32>
+! CHECK:         omp.teams {
+! CHECK:           %[[GP:.*]] = omp.groupprivate @_QMgp_modEgp_x device_type (host) : !fir.ref<i32>
+! CHECK:           %[[DECL:.*]]:2 = hlfir.declare %[[GP]] {uniq_name = "_QMgp_modEgp_x"} : (!fir.ref<i32>) -> (!fir.ref<i32>, !fir.ref<i32>)
+! CHECK:           %[[C42:.*]] = arith.constant 42 : i32
+! CHECK:           hlfir.assign %[[C42]] to %[[DECL]]#0 : i32, !fir.ref<i32>
+! CHECK:           omp.terminator
+! CHECK:         }
diff --git a/flang/test/Lower/OpenMP/groupprivate.f90 b/flang/test/Lower/OpenMP/groupprivate.f90
new file mode 100644
index 0000000..f84f3f2
--- /dev/null
+++ b/flang/test/Lower/OpenMP/groupprivate.f90
@@ -0,0 +1,276 @@
+!RUN: %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=60 %s -o - | FileCheck %s
+
+! Test lowering of groupprivate directive to omp.groupprivate.
+
+! CHECK-DAG: fir.global common @blk_
+! CHECK-DAG: fir.global common @blka_
+! CHECK-DAG: fir.global common @blkb_
+
+! Test 1: Basic groupprivate with single module variable.
+module m
+  implicit none
+  integer, save :: x
+  !$omp groupprivate(x)
+end module
+
+! CHECK-LABEL: func.func @_QPtest_groupprivate
+! CHECK:         omp.target {
+! CHECK:           omp.teams {
+! CHECK:             %[[GP:.*]] = omp.groupprivate @_QMmEx device_type (any) : !fir.ref<i32>
+! CHECK:             %[[DECL:.*]]:2 = hlfir.declare %[[GP]] {uniq_name = "_QMmEx"} : (!fir.ref<i32>) -> (!fir.ref<i32>, !fir.ref<i32>)
+! CHECK:             %[[C10:.*]] = arith.constant 10 : i32
+! CHECK:             hlfir.assign %[[C10]] to %[[DECL]]#0 : i32, !fir.ref<i32>
+subroutine test_groupprivate()
+  use m
+
+  !$omp target
+    !$omp teams
+      x = 10
+    !$omp end teams
+  !$omp end target
+end subroutine
+
+! Test 2: Groupprivate with common block.
+module m2
+  implicit none
+  integer :: cb_x, cb_y
+  real :: cb_z
+  common /blk/ cb_x, cb_y, cb_z
+  !$omp groupprivate(/blk/)
+end module
+
+! CHECK-LABEL: func.func @_QPtest_common_block_groupprivate
+! CHECK:         omp.target {
+! CHECK:           omp.teams {
+! CHECK:             %[[GP:.*]] = omp.groupprivate @blk_ device_type (any) : !fir.ref<!fir.array<12xi8>>
+! CHECK:             %[[DECL_X:.*]]:2 = hlfir.declare %{{.*}} storage(%[[GP]][0]) {uniq_name = "_QMm2Ecb_x"}
+! CHECK:             %[[DECL_Y:.*]]:2 = hlfir.declare %{{.*}} storage(%[[GP]][4]) {uniq_name = "_QMm2Ecb_y"}
+! CHECK:             %[[DECL_Z:.*]]:2 = hlfir.declare %{{.*}} storage(%[[GP]][8]) {uniq_name = "_QMm2Ecb_z"}
+! CHECK:             hlfir.assign %{{.*}} to %[[DECL_X]]#0 : i32, !fir.ref<i32>
+! CHECK:             hlfir.assign %{{.*}} to %[[DECL_Y]]#0 : i32, !fir.ref<i32>
+! CHECK:             hlfir.assign %{{.*}} to %[[DECL_Z]]#0 : f32, !fir.ref<f32>
+subroutine test_common_block_groupprivate()
+  use m2
+
+  !$omp target
+    !$omp teams
+      cb_x = 1
+      cb_y = 2
+      cb_z = 3.0
+    !$omp end teams
+  !$omp end target
+end subroutine
+
+! Test 3: Local SAVE variable promoted to fir.global by globalInitialization.
+! CHECK-LABEL: func.func @_QPtest_local_save_groupprivate
+! CHECK:         omp.teams {
+! CHECK:           %[[GP:.*]] = omp.groupprivate @_QFtest_local_save_groupprivateElocal_x device_type (any) : !fir.ref<i32>
+! CHECK:           %[[DECL:.*]]:2 = hlfir.declare %[[GP]] {uniq_name = "_QFtest_local_save_groupprivateElocal_x"} : (!fir.ref<i32>) -> (!fir.ref<i32>, !fir.ref<i32>)
+! CHECK:           %[[C42:.*]] = arith.constant 42 : i32
+! CHECK:           hlfir.assign %[[C42]] to %[[DECL]]#0 : i32, !fir.ref<i32>
+subroutine test_local_save_groupprivate()
+  integer, save :: local_x
+  !$omp groupprivate(local_x)
+
+  !$omp teams
+    local_x = 42
+  !$omp end teams
+end subroutine
+
+! Test 4: Multiple groupprivate variables in same teams region.
+module m_multi
+  implicit none
+  integer, save :: gp_a
+  integer, save :: gp_b
+  real,    save :: gp_c
+  !$omp groupprivate(gp_a, gp_b, gp_c)
+end module
+
+! CHECK-LABEL: func.func @_QPtest_multiple_groupprivate
+! CHECK:         omp.target {
+! CHECK:           omp.teams {
+! CHECK:             %[[GP_A:.*]] = omp.groupprivate @_QMm_multiEgp_a device_type (any) : !fir.ref<i32>
+! CHECK:             %[[DECL_A:.*]]:2 = hlfir.declare %[[GP_A]] {uniq_name = "_QMm_multiEgp_a"} : (!fir.ref<i32>) -> (!fir.ref<i32>, !fir.ref<i32>)
+! CHECK:             %[[GP_B:.*]] = omp.groupprivate @_QMm_multiEgp_b device_type (any) : !fir.ref<i32>
+! CHECK:             %[[DECL_B:.*]]:2 = hlfir.declare %[[GP_B]] {uniq_name = "_QMm_multiEgp_b"} : (!fir.ref<i32>) -> (!fir.ref<i32>, !fir.ref<i32>)
+! CHECK:             %[[GP_C:.*]] = omp.groupprivate @_QMm_multiEgp_c device_type (any) : !fir.ref<f32>
+! CHECK:             %[[DECL_C:.*]]:2 = hlfir.declare %[[GP_C]] {uniq_name = "_QMm_multiEgp_c"} : (!fir.ref<f32>) -> (!fir.ref<f32>, !fir.ref<f32>)
+! CHECK:             hlfir.assign %{{.*}} to %[[DECL_A]]#0 : i32, !fir.ref<i32>
+! CHECK:             hlfir.assign %{{.*}} to %[[DECL_B]]#0 : i32, !fir.ref<i32>
+! CHECK:             hlfir.assign %{{.*}} to %[[DECL_C]]#0 : f32, !fir.ref<f32>
+subroutine test_multiple_groupprivate()
+  use m_multi
+
+  !$omp target
+    !$omp teams
+      gp_a = 1
+      gp_b = 2
+      gp_c = 3.0
+    !$omp end teams
+  !$omp end target
+end subroutine
+
+! Test 5: Same variable referenced multiple times produces only one op, and
+! every reference accesses the per-team copy via the same hlfir.declare.
+! CHECK-LABEL: func.func @_QPtest_repeated_ref_groupprivate
+! CHECK:         omp.teams {
+! CHECK:           %[[GP:.*]] = omp.groupprivate @_QMmEx device_type (any) : !fir.ref<i32>
+! CHECK:           %[[DECL:.*]]:2 = hlfir.declare %[[GP]] {uniq_name = "_QMmEx"} : (!fir.ref<i32>) -> (!fir.ref<i32>, !fir.ref<i32>)
+! CHECK-NOT:       omp.groupprivate @_QMmEx
+! CHECK:           hlfir.assign %{{.*}} to %[[DECL]]#0 : i32, !fir.ref<i32>
+! CHECK:           %{{.*}} = fir.load %[[DECL]]#0 : !fir.ref<i32>
+! CHECK:           hlfir.assign %{{.*}} to %[[DECL]]#0 : i32, !fir.ref<i32>
+! CHECK:           %{{.*}} = fir.load %[[DECL]]#0 : !fir.ref<i32>
+! CHECK:           hlfir.assign %{{.*}} to %[[DECL]]#0 : i32, !fir.ref<i32>
+! CHECK:           omp.terminator
+subroutine test_repeated_ref_groupprivate()
+  use m
+
+  !$omp target
+    !$omp teams
+      x = 10
+      x = x + 5
+      x = x * 2
+    !$omp end teams
+  !$omp end target
+end subroutine
+
+! Test 6: Standalone teams (no enclosing target) still triggers groupprivate.
+! CHECK-LABEL: func.func @_QPtest_standalone_teams_groupprivate
+! CHECK-NOT:     omp.target
+! CHECK:         omp.teams {
+! CHECK:           %[[GP:.*]] = omp.groupprivate @_QMmEx device_type (any) : !fir.ref<i32>
+! CHECK:           %[[DECL:.*]]:2 = hlfir.declare %[[GP]] {uniq_name = "_QMmEx"} : (!fir.ref<i32>) -> (!fir.ref<i32>, !fir.ref<i32>)
+! CHECK:           %[[C100:.*]] = arith.constant 100 : i32
+! CHECK:           hlfir.assign %[[C100]] to %[[DECL]]#0 : i32, !fir.ref<i32>
+subroutine test_standalone_teams_groupprivate()
+  use m
+
+  !$omp teams
+    x = 100
+  !$omp end teams
+end subroutine
+
+! Test 7: Groupprivate variable is not added to target's implicit map_entries,
+! and the access inside the teams region goes through the omp.groupprivate
+! result rather than the original host global.
+! CHECK-LABEL: func.func @_QPtest_target_skip_map_groupprivate
+! CHECK-NOT:     omp.map.info
+! CHECK:         omp.target {
+! CHECK:           omp.teams {
+! CHECK:             %[[GP:.*]] = omp.groupprivate @_QMmEx device_type (any) : !fir.ref<i32>
+! CHECK:             %[[DECL:.*]]:2 = hlfir.declare %[[GP]] {uniq_name = "_QMmEx"} : (!fir.ref<i32>) -> (!fir.ref<i32>, !fir.ref<i32>)
+! CHECK:             %[[C200:.*]] = arith.constant 200 : i32
+! CHECK:             hlfir.assign %[[C200]] to %[[DECL]]#0 : i32, !fir.ref<i32>
+subroutine test_target_skip_map_groupprivate()
+  use m
+
+  !$omp target
+    !$omp teams
+      x = 200
+    !$omp end teams
+  !$omp end target
+end subroutine
+
+! Test 8: Groupprivate works with various element types (scalar, array, real).
+module m_types
+  implicit none
+  real(8), save :: gp_r8
+  integer, save :: gp_iarr(4)
+  !$omp groupprivate(gp_r8, gp_iarr)
+end module
+
+! CHECK-LABEL: func.func @_QPtest_types_groupprivate
+! CHECK:         omp.teams {
+! CHECK:           %[[GP_R8:.*]] = omp.groupprivate @_QMm_typesEgp_r8 device_type (any) : !fir.ref<f64>
+! CHECK:           %[[DECL_R8:.*]]:2 = hlfir.declare %[[GP_R8]] {uniq_name = "_QMm_typesEgp_r8"} : (!fir.ref<f64>) -> (!fir.ref<f64>, !fir.ref<f64>)
+! CHECK:           %[[GP_IARR:.*]] = omp.groupprivate @_QMm_typesEgp_iarr device_type (any) : !fir.ref<!fir.array<4xi32>>
+! CHECK:           %[[SHAPE:.*]] = fir.shape %{{.*}} : (index) -> !fir.shape<1>
+! CHECK:           %[[DECL_IARR:.*]]:2 = hlfir.declare %[[GP_IARR]](%[[SHAPE]]) {uniq_name = "_QMm_typesEgp_iarr"}
+! CHECK:           hlfir.assign %{{.*}} to %[[DECL_R8]]#0 : f64, !fir.ref<f64>
+! CHECK:           %[[ELT:.*]] = hlfir.designate %[[DECL_IARR]]#0 (%{{.*}}) : (!fir.ref<!fir.array<4xi32>>, index) -> !fir.ref<i32>
+! CHECK:           hlfir.assign %{{.*}} to %[[ELT]] : i32, !fir.ref<i32>
+subroutine test_types_groupprivate()
+  use m_types
+
+  !$omp teams
+    gp_r8 = 1.0d0
+    gp_iarr(1) = 99
+  !$omp end teams
+end subroutine
+
+! Test 9: Multiple distinct common blocks each get their own omp.groupprivate.
+module m_blocks
+  implicit none
+  integer :: a1, a2
+  integer :: b1, b2
+  common /blka/ a1, a2
+  common /blkb/ b1, b2
+  !$omp groupprivate(/blka/, /blkb/)
+end module
+
+! CHECK-LABEL: func.func @_QPtest_multi_common_groupprivate
+! CHECK:         omp.teams {
+! CHECK:           %[[GP_A:.*]] = omp.groupprivate @blka_ device_type (any) : !fir.ref<!fir.array<8xi8>>
+! CHECK:           %[[DECL_A1:.*]]:2 = hlfir.declare %{{.*}} storage(%[[GP_A]][0]) {uniq_name = "_QMm_blocksEa1"}
+! CHECK:           %[[GP_B:.*]] = omp.groupprivate @blkb_ device_type (any) : !fir.ref<!fir.array<8xi8>>
+! CHECK:           %[[DECL_B1:.*]]:2 = hlfir.declare %{{.*}} storage(%[[GP_B]][0]) {uniq_name = "_QMm_blocksEb1"}
+! CHECK:           hlfir.assign %{{.*}} to %[[DECL_A1]]#0 : i32, !fir.ref<i32>
+! CHECK:           hlfir.assign %{{.*}} to %[[DECL_B1]]#0 : i32, !fir.ref<i32>
+subroutine test_multi_common_groupprivate()
+  use m_blocks
+
+  !$omp teams
+    a1 = 1
+    b1 = 2
+  !$omp end teams
+end subroutine
+
+! Test 10: device_type(host) and device_type(nohost) clauses are honored.
+module m_dt
+  implicit none
+  integer, save :: gp_h
+  integer, save :: gp_nh
+  !$omp groupprivate(gp_h)  device_type(host)
+  !$omp groupprivate(gp_nh) device_type(nohost)
+end module
+
+! CHECK-LABEL: func.func @_QPtest_device_type_groupprivate
+! CHECK:         omp.teams {
+! CHECK:           %[[GP_H:.*]] = omp.groupprivate @_QMm_dtEgp_h device_type (host) : !fir.ref<i32>
+! CHECK:           %[[DECL_H:.*]]:2 = hlfir.declare %[[GP_H]] {uniq_name = "_QMm_dtEgp_h"} : (!fir.ref<i32>) -> (!fir.ref<i32>, !fir.ref<i32>)
+! CHECK:           %[[GP_NH:.*]] = omp.groupprivate @_QMm_dtEgp_nh device_type (nohost) : !fir.ref<i32>
+! CHECK:           %[[DECL_NH:.*]]:2 = hlfir.declare %[[GP_NH]] {uniq_name = "_QMm_dtEgp_nh"} : (!fir.ref<i32>) -> (!fir.ref<i32>, !fir.ref<i32>)
+! CHECK:           hlfir.assign %{{.*}} to %[[DECL_H]]#0 : i32, !fir.ref<i32>
+! CHECK:           hlfir.assign %{{.*}} to %[[DECL_NH]]#0 : i32, !fir.ref<i32>
+subroutine test_device_type_groupprivate()
+  use m_dt
+
+  !$omp teams
+    gp_h = 1
+    gp_nh = 2
+  !$omp end teams
+end subroutine
+
+! Test 11: The module owning the !$omp groupprivate directive
+! is declared AFTER a subroutine that already
+! references the variable inside a teams region.
+! CHECK-LABEL: func.func @_QPtest_module_after_subroutine_groupprivate
+! CHECK:         omp.teams {
+! CHECK:           %[[GP:.*]] = omp.groupprivate @_QMm_lateEgp_late device_type (host) : !fir.ref<i32>
+! CHECK:           %[[DECL:.*]]:2 = hlfir.declare %[[GP]] {uniq_name = "_QMm_lateEgp_late"} : (!fir.ref<i32>) -> (!fir.ref<i32>, !fir.ref<i32>)
+! CHECK:           %[[C7:.*]] = arith.constant 7 : i32
+! CHECK:           hlfir.assign %[[C7]] to %[[DECL]]#0 : i32, !fir.ref<i32>
+subroutine test_module_after_subroutine_groupprivate()
+  use m_late
+
+  !$omp teams
+    gp_late = 7
+  !$omp end teams
+end subroutine
+
+module m_late
+  implicit none
+  integer, save :: gp_late
+  !$omp groupprivate(gp_late) device_type(host)
+end module