[flang-commits] [clang] [flang] [flang][openacc] Attach firstprivate scalar onto combined parallel loop (PR #222777)

Susan Tan ス-ザン タン via flang-commits flang-commits at lists.llvm.org
Mon Sep 14 13:49:37 PDT 2026


https://github.com/SusanTan updated https://github.com/llvm/llvm-project/pull/222777

>From 85b75b94bd515b5ae3f28c00515948559dd93d03 Mon Sep 17 00:00:00 2001
From: Susan Tan <zujunt at nvidia.com>
Date: Thu, 10 Sep 2026 13:51:15 -0700
Subject: [PATCH 01/13] impl

---
 flang/lib/Lower/OpenACC.cpp                   |  68 +++++++++---
 .../test/Lower/OpenACC/acc-parallel-loop.f90  | 100 ++++++++++++++++++
 flang/test/Lower/OpenACC/acc-private.f90      |   3 +-
 flang/test/Lower/OpenACC/acc-serial-loop.f90  |  19 ++++
 4 files changed, 177 insertions(+), 13 deletions(-)

diff --git a/flang/lib/Lower/OpenACC.cpp b/flang/lib/Lower/OpenACC.cpp
index d883478e49375..4f350de1b6a8c 100644
--- a/flang/lib/Lower/OpenACC.cpp
+++ b/flang/lib/Lower/OpenACC.cpp
@@ -1722,6 +1722,31 @@ loopWillBeIndependent(Fortran::lower::AbstractConverter &converter,
   }
 }
 
+// Attach an implicit firstprivate on this combined loop in addition to the
+// compute clause (firstprivate is not a loop clause in the spec) when:
+//  1. Combined `parallel loop` — the region is that one loop. Not `serial
+//     loop` (seq by default), `kernels loop`, or a standalone `acc loop`.
+//  2. Independent (the `parallel loop` default). Not `seq`/`auto`, which
+//     must keep a carried firstprivate.
+//  3. Scalar. Nested loops reuse this copy via remap; they get no clause.
+static bool shouldAttachFirstprivateOnCombinedLoop(
+    Fortran::lower::AbstractConverter &converter,
+    const Fortran::parser::AccClauseList &accClauseList,
+    std::optional<mlir::acc::CombinedConstructsType> combinedConstructs,
+    const Fortran::parser::AccObject &accObject) {
+  if (!combinedConstructs ||
+      *combinedConstructs != mlir::acc::CombinedConstructsType::ParallelLoop)
+    return false;
+  if (!loopWillBeIndependent(converter, accClauseList,
+                             llvm::acc::ACCD_parallel_loop))
+    return false;
+  mlir::Value var =
+      converter.getSymbolAddress(getSymbolFromAccObject(accObject));
+  return var &&
+         mlir::acc::bitEnumContainsAny(mlir::acc::getTypeCategory(var),
+                                       mlir::acc::VariableTypeCategory::scalar);
+}
+
 // Helper to visit Bounds of DO LOOP nest.
 //
 // When `markInnerCollapsed` is true (the default), inner DOs that are absorbed
@@ -2209,6 +2234,7 @@ buildACCLoopOp(Fortran::lower::AbstractConverter &converter,
                const Fortran::parser::DoConstruct &outerDoConstruct,
                Fortran::lower::pft::Evaluation &eval,
                llvm::SmallVector<mlir::Value> &privateOperands,
+               llvm::SmallVector<mlir::Value> &firstprivateOperands,
                AccDataMap &dataMap,
                llvm::SmallVector<mlir::Value> &gangOperands,
                llvm::SmallVector<mlir::Value> &workerNumOperands,
@@ -2227,7 +2253,6 @@ buildACCLoopOp(Fortran::lower::AbstractConverter &converter,
   llvm::SmallVector<bool> inclusiveBounds;
   llvm::SmallVector<mlir::Location> locs;
   llvm::SmallVector<mlir::Value> lowerbounds, upperbounds, steps;
-  llvm::SmallVector<mlir::Value> firstprivateOperands;
   llvm::SmallVector<
       std::pair<Fortran::semantics::SymbolRef, Fortran::semantics::SymbolRef>>
       localSymPairs;
@@ -2369,8 +2394,8 @@ static mlir::acc::LoopOp createLoopOp(
         std::nullopt) {
   fir::FirOpBuilder &builder = converter.getFirOpBuilder();
   llvm::SmallVector<mlir::Value> tileOperands, privateOperands,
-      reductionOperands, cacheOperands, vectorOperands, workerNumOperands,
-      gangOperands;
+      firstprivateOperands, reductionOperands, cacheOperands, vectorOperands,
+      workerNumOperands, gangOperands;
   llvm::SmallVector<int32_t> tileOperandsSegments, gangOperandsSegments;
   llvm::SmallVector<int64_t> collapseValues;
 
@@ -2499,6 +2524,24 @@ static mlir::acc::LoopOp createLoopOp(
           /*structured=*/true, /*implicit=*/false,
           /*async=*/{}, /*asyncDeviceTypes=*/{}, /*asyncOnlyDeviceTypes=*/{},
           /*setDeclareAttr=*/false, &dataMap);
+    } else if (const auto *firstprivateClause =
+                   std::get_if<Fortran::parser::AccClause::Firstprivate>(
+                       &clause.u)) {
+      // Duplicate scalar firstprivate onto this combined independent loop.
+      // The compute construct already has the user-facing firstprivate (host
+      // seed). After that remap, getSymbolAddress is the compute copy, so the
+      // loop clause's varPtr chains from it. implicit=true: firstprivate is
+      // not a loop clause in the spec.
+      genDataOperandOperations<mlir::acc::FirstprivateOp>(
+          firstprivateClause->v, converter, semanticsContext, stmtCtx,
+          firstprivateOperands, mlir::acc::DataClause::acc_firstprivate,
+          /*structured=*/true, /*implicit=*/true,
+          /*async=*/{}, /*asyncDeviceTypes=*/{}, /*asyncOnlyDeviceTypes=*/{},
+          /*setDeclareAttr=*/false, &dataMap,
+          /*filter=*/[&](const Fortran::parser::AccObject &obj) {
+            return shouldAttachFirstprivateOnCombinedLoop(
+                converter, accClauseList, combinedConstructs, obj);
+          });
     } else if (const auto *reductionClause =
                    std::get_if<Fortran::parser::AccClause::Reduction>(
                        &clause.u)) {
@@ -2572,9 +2615,10 @@ static mlir::acc::LoopOp createLoopOp(
 
   auto loopOp = buildACCLoopOp(
       converter, currentLocation, semanticsContext, stmtCtx, outerDoConstruct,
-      eval, privateOperands, dataMap, gangOperands, workerNumOperands,
-      vectorOperands, tileOperands, cacheOperands, reductionOperands, retTy,
-      yieldValue, loopsToProcess, /*hasDirective=*/true);
+      eval, privateOperands, firstprivateOperands, dataMap, gangOperands,
+      workerNumOperands, vectorOperands, tileOperands, cacheOperands,
+      reductionOperands, retTy, yieldValue, loopsToProcess,
+      /*hasDirective=*/true);
 
   if (!gangDeviceTypes.empty())
     loopOp.setGangAttr(builder.getArrayAttr(gangDeviceTypes));
@@ -5555,9 +5599,9 @@ mlir::Operation *Fortran::lower::genOpenACCLoopFromDoConstruct(
 
   // Prepare empty operand vectors since there are no associated `acc loop`
   // clauses with the Fortran do loops being handled here.
-  llvm::SmallVector<mlir::Value> privateOperands, gangOperands,
-      workerNumOperands, vectorOperands, tileOperands, cacheOperands,
-      reductionOperands;
+  llvm::SmallVector<mlir::Value> privateOperands, firstprivateOperands,
+      gangOperands, workerNumOperands, vectorOperands, tileOperands,
+      cacheOperands, reductionOperands;
   llvm::SmallVector<mlir::Type> retTy;
   AccDataMap dataMap;
   mlir::Value yieldValue;
@@ -5568,9 +5612,9 @@ mlir::Operation *Fortran::lower::genOpenACCLoopFromDoConstruct(
   Fortran::lower::StatementContext stmtCtx;
   auto loopOp = buildACCLoopOp(
       converter, converter.getCurrentLocation(), semanticsContext, stmtCtx,
-      doConstruct, eval, privateOperands, dataMap, gangOperands,
-      workerNumOperands, vectorOperands, tileOperands, cacheOperands,
-      reductionOperands, retTy, yieldValue, loopsToProcess,
+      doConstruct, eval, privateOperands, firstprivateOperands, dataMap,
+      gangOperands, workerNumOperands, vectorOperands, tileOperands,
+      cacheOperands, reductionOperands, retTy, yieldValue, loopsToProcess,
       /*hasDirective=*/false);
 
   // Normal do loops which are not annotated with `acc loop` should be
diff --git a/flang/test/Lower/OpenACC/acc-parallel-loop.f90 b/flang/test/Lower/OpenACC/acc-parallel-loop.f90
index 8b163593fede7..6017aec832b6b 100644
--- a/flang/test/Lower/OpenACC/acc-parallel-loop.f90
+++ b/flang/test/Lower/OpenACC/acc-parallel-loop.f90
@@ -453,6 +453,7 @@ subroutine acc_parallel_loop
 
 ! CHECK:      %[[ACC_PRIVATE_B:.*]] = acc.firstprivate varPtr(%[[DECLB]]#0 : !fir.ref<!fir.array<10xf32>>) recipe(@firstprivatization_ref_10xf32) name("b") -> !fir.ref<!fir.array<10xf32>>
 ! CHECK:      acc.parallel {{.*}} firstprivate(%[[ACC_PRIVATE_B]] : !fir.ref<!fir.array<10xf32>>) {
+! CHECK-NOT:    acc.firstprivate {{.*}} implicit(true)
 ! CHECK:        %[[ACC_PRIVATE_A:.*]] = acc.private varPtr(%[[DECLA]]#0 : !fir.ref<!fir.array<10xf32>>) recipe(@privatization_ref_10xf32) name("a") -> !fir.ref<!fir.array<10xf32>>
 ! CHECK:        acc.loop {{.*}} private(%[[ACC_PRIVATE_A]]{{.*}} : !fir.ref<!fir.array<10xf32>>{{.*}})
 ! CHECK-NOT:      fir.do_loop
@@ -671,3 +672,102 @@ subroutine acc_parallel_loop
 ! CHECK-NOT: fir.do_loop
 
 end subroutine acc_parallel_loop
+
+! Combined parallel loop firstprivate: scalar copies are also attached on the
+! independent acc.loop; arrays, seq loops, and non-combined sibling loops are
+! not.
+subroutine acc_parallel_loop_firstprivate_scalar
+  integer :: i, n, v
+  real :: a(10)
+  n = 10
+  v = 7
+  !$acc parallel loop firstprivate(v)
+  do i = 1, n
+    a(i) = v
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_scalar
+! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
+! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) implicit(true) name("v") -> !fir.ref<i32>
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<i32>)
+! CHECK: } inclusiveUpperbound(array<i1: true>) independent
+
+subroutine acc_parallel_loop_firstprivate_seq
+  integer :: i, n, v
+  real :: a(10)
+  n = 10
+  v = 7
+  !$acc parallel loop seq firstprivate(v)
+  do i = 1, n
+    a(i) = v
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_seq
+! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop combined(parallel)
+! CHECK: } inclusiveUpperbound(array<i1: true>) seq
+
+subroutine acc_parallel_loop_firstprivate_auto
+  integer :: i, n, v
+  real :: a(10)
+  n = 10
+  v = 7
+  !$acc parallel loop auto firstprivate(v)
+  do i = 1, n
+    a(i) = v
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_auto
+! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop combined(parallel)
+! CHECK: } inclusiveUpperbound(array<i1: true>) auto_
+
+subroutine acc_parallel_loop_firstprivate_mixed
+  integer :: i, n, v
+  real :: b(10)
+  n = 10
+  v = 7
+  !$acc parallel loop firstprivate(v, b)
+  do i = 1, n
+    b(i) = v
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_mixed
+! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
+! CHECK: %[[FP_B:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<!fir.array<10xf32>>) recipe({{.*}}) name("b") -> !fir.ref<!fir.array<10xf32>>
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_V]], %[[FP_B]] : !fir.ref<i32>, !fir.ref<!fir.array<10xf32>>)
+! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) implicit(true) name("v") -> !fir.ref<i32>
+! CHECK-NOT: implicit(true) name("b")
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<i32>)
+
+subroutine acc_parallel_firstprivate_sibling_loops
+  integer :: i, j, n, v
+  real :: a(10)
+  n = 10
+  v = 7
+  !$acc parallel firstprivate(v)
+  !$acc loop
+  do i = 1, n
+    a(i) = v
+  end do
+  !$acc loop
+  do j = 1, n
+    a(j) = v
+  end do
+  !$acc end parallel
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_firstprivate_sibling_loops
+! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
+! CHECK: acc.parallel {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop
diff --git a/flang/test/Lower/OpenACC/acc-private.f90 b/flang/test/Lower/OpenACC/acc-private.f90
index fdebdc2ed6f2a..aa5ebc5e3a5d2 100644
--- a/flang/test/Lower/OpenACC/acc-private.f90
+++ b/flang/test/Lower/OpenACC/acc-private.f90
@@ -425,7 +425,8 @@ program acc_private
 
 ! CHECK: %[[FP_C:.*]] = acc.firstprivate varPtr(%[[DECLC]]#0 : !fir.ref<i32>) recipe(@firstprivatization_ref_i32) name("c") -> !fir.ref<i32>
 ! CHECK: acc.parallel {{.*}} firstprivate(%[[FP_C]] : !fir.ref<i32>)
-! CHECK: acc.yield
+! CHECK: %[[FP_C_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe(@firstprivatization_ref_i32) implicit(true) name("c") -> !fir.ref<i32>
+! CHECK: acc.loop {{.*}}firstprivate(%[[FP_C_LOOP]] : !fir.ref<i32>)
 
   !$acc parallel loop firstprivate(b)
   DO i = 1, n
diff --git a/flang/test/Lower/OpenACC/acc-serial-loop.f90 b/flang/test/Lower/OpenACC/acc-serial-loop.f90
index 46ab7b3d37302..703cfe79ee28e 100644
--- a/flang/test/Lower/OpenACC/acc-serial-loop.f90
+++ b/flang/test/Lower/OpenACC/acc-serial-loop.f90
@@ -601,3 +601,22 @@ subroutine acc_serial_loop
 ! CHECK:      acc.copyout accPtr(%[[COPYINREDI]] : !fir.ref<i32>) to varPtr(%{{.*}} : !fir.ref<i32>) dataClause(acc_reduction) implicit(true) name("reduction_i")
 
 end subroutine acc_serial_loop
+
+! serial loop defaults to seq, so scalar firstprivate stays on the compute
+! construct only.
+subroutine acc_serial_loop_firstprivate_scalar
+  integer :: i, n, v
+  real :: a(10)
+  n = 10
+  v = 7
+  !$acc serial loop firstprivate(v)
+  do i = 1, n
+    a(i) = v
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_serial_loop_firstprivate_scalar
+! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
+! CHECK: acc.serial combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop combined(serial)

>From b69946918f79b987c52ef800de0d7c379dc28fcc Mon Sep 17 00:00:00 2001
From: Susan Tan <zujunt at nvidia.com>
Date: Thu, 10 Sep 2026 14:03:43 -0700
Subject: [PATCH 02/13] add tests

---
 .../test/Lower/OpenACC/acc-parallel-loop.f90  | 284 +++++++++++++++++-
 flang/test/Lower/OpenACC/acc-private.f90      |   9 +
 flang/test/Lower/OpenACC/acc-serial-loop.f90  |  19 ++
 3 files changed, 310 insertions(+), 2 deletions(-)

diff --git a/flang/test/Lower/OpenACC/acc-parallel-loop.f90 b/flang/test/Lower/OpenACC/acc-parallel-loop.f90
index 6017aec832b6b..8c98e17628b48 100644
--- a/flang/test/Lower/OpenACC/acc-parallel-loop.f90
+++ b/flang/test/Lower/OpenACC/acc-parallel-loop.f90
@@ -674,8 +674,8 @@ subroutine acc_parallel_loop
 end subroutine acc_parallel_loop
 
 ! Combined parallel loop firstprivate: scalar copies are also attached on the
-! independent acc.loop; arrays, seq loops, and non-combined sibling loops are
-! not.
+! independent acc.loop. Arrays, sections, derived/character/boxed types, seq
+! and auto loops, and non-combined loops are not.
 subroutine acc_parallel_loop_firstprivate_scalar
   integer :: i, n, v
   real :: a(10)
@@ -771,3 +771,283 @@ subroutine acc_parallel_firstprivate_sibling_loops
 ! CHECK: acc.parallel {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
 ! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
 ! CHECK: acc.loop
+
+! Explicit independent is the same as the parallel loop default.
+subroutine acc_parallel_loop_firstprivate_independent
+  integer :: i, n, v
+  real :: a(10)
+  n = 10
+  v = 7
+  !$acc parallel loop independent firstprivate(v)
+  do i = 1, n
+    a(i) = v
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_independent
+! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
+! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) implicit(true) name("v") -> !fir.ref<i32>
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<i32>)
+! CHECK: } inclusiveUpperbound(array<i1: true>) independent
+
+! gang does not change independent, so the scalar still copies onto the loop.
+subroutine acc_parallel_loop_firstprivate_gang
+  integer :: i, n, v
+  real :: a(10)
+  n = 10
+  v = 7
+  !$acc parallel loop gang firstprivate(v)
+  do i = 1, n
+    a(i) = v
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_gang
+! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
+! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) implicit(true) name("v") -> !fir.ref<i32>
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<i32>)
+
+! collapse is still one acc.loop; attach the scalar once.
+subroutine acc_parallel_loop_firstprivate_collapse
+  integer :: i, j, n, v
+  real :: a(10, 10)
+  n = 10
+  v = 7
+  !$acc parallel loop collapse(2) firstprivate(v)
+  do i = 1, n
+    do j = 1, n
+      a(i, j) = v
+    end do
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_collapse
+! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
+! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) implicit(true) name("v") -> !fir.ref<i32>
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<i32>)
+! CHECK: collapse([2])
+
+! Inner standalone loop reuses the outer copy; no extra firstprivate.
+subroutine acc_parallel_loop_firstprivate_nested
+  integer :: i, j, n, v
+  real :: a(10, 10)
+  n = 10
+  v = 7
+  !$acc parallel loop firstprivate(v)
+  do i = 1, n
+    !$acc loop
+    do j = 1, n
+      a(i, j) = v
+    end do
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_nested
+! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
+! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) implicit(true) name("v") -> !fir.ref<i32>
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<i32>)
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop
+
+subroutine acc_parallel_loop_firstprivate_two_scalars
+  integer :: i, n, v, w
+  real :: a(10)
+  n = 10
+  v = 7
+  w = 3
+  !$acc parallel loop firstprivate(v, w)
+  do i = 1, n
+    a(i) = v + w
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_two_scalars
+! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
+! CHECK: %[[FP_W:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("w") -> !fir.ref<i32>
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_V]], %[[FP_W]] : !fir.ref<i32>, !fir.ref<i32>)
+! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) implicit(true) name("v") -> !fir.ref<i32>
+! CHECK: %[[FP_W_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) implicit(true) name("w") -> !fir.ref<i32>
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_V_LOOP]], %[[FP_W_LOOP]] : !fir.ref<i32>, !fir.ref<i32>)
+
+subroutine acc_parallel_loop_firstprivate_scalar_kinds
+  integer :: i, n
+  real :: x
+  logical :: l
+  complex :: z
+  real :: a(10)
+  n = 10
+  x = 1.0
+  l = .true.
+  z = (1.0, 0.0)
+  !$acc parallel loop firstprivate(x, l, z)
+  do i = 1, n
+    a(i) = x
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_scalar_kinds
+! CHECK: %[[FP_X:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<f32>) recipe({{.*}}) name("x") -> !fir.ref<f32>
+! CHECK: %[[FP_L:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<!fir.logical<4>>) recipe({{.*}}) name("l") -> !fir.ref<!fir.logical<4>>
+! CHECK: %[[FP_Z:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<complex<f32>>) recipe({{.*}}) name("z") -> !fir.ref<complex<f32>>
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_X]], %[[FP_L]], %[[FP_Z]] : !fir.ref<f32>, !fir.ref<!fir.logical<4>>, !fir.ref<complex<f32>>)
+! CHECK: %[[FP_X_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<f32>) recipe({{.*}}) implicit(true) name("x") -> !fir.ref<f32>
+! CHECK: %[[FP_L_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<!fir.logical<4>>) recipe({{.*}}) implicit(true) name("l") -> !fir.ref<!fir.logical<4>>
+! CHECK: %[[FP_Z_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<complex<f32>>) recipe({{.*}}) implicit(true) name("z") -> !fir.ref<complex<f32>>
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_X_LOOP]], %[[FP_L_LOOP]], %[[FP_Z_LOOP]] : !fir.ref<f32>, !fir.ref<!fir.logical<4>>, !fir.ref<complex<f32>>)
+
+subroutine acc_parallel_loop_firstprivate_dummy(v)
+  integer :: v
+  integer :: i, n
+  real :: a(10)
+  n = 10
+  !$acc parallel loop firstprivate(v)
+  do i = 1, n
+    a(i) = v
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_dummy
+! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
+! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) implicit(true) name("v") -> !fir.ref<i32>
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<i32>)
+
+subroutine acc_parallel_loop_firstprivate_with_reduction
+  integer :: i, n, v, r
+  n = 10
+  v = 7
+  r = 0
+  !$acc parallel loop firstprivate(v) reduction(+:r)
+  do i = 1, n
+    r = r + v
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_with_reduction
+! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
+! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) implicit(true) name("v") -> !fir.ref<i32>
+! CHECK: %[[RED_R:.*]] = acc.reduction varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) name("r") -> !fir.ref<i32>
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<i32>) reduction(%[[RED_R]] : !fir.ref<i32>)
+
+! Character is not a scalar type category.
+subroutine acc_parallel_loop_firstprivate_character
+  integer :: i, n
+  character(len=8) :: s
+  real :: a(10)
+  n = 10
+  s = 'xxxxxxxx'
+  !$acc parallel loop firstprivate(s)
+  do i = 1, n
+    a(i) = 1.0
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_character
+! CHECK: %[[FP_S:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<!fir.char<1,8>>) recipe({{.*}}) name("s") -> !fir.ref<!fir.char<1,8>>
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_S]] : !fir.ref<!fir.char<1,8>>)
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop combined(parallel)
+
+! Derived type is composite, not scalar.
+subroutine acc_parallel_loop_firstprivate_derived
+  type t
+    integer :: x
+  end type
+  type(t) :: p
+  integer :: i, n
+  real :: a(10)
+  n = 10
+  p%x = 1
+  !$acc parallel loop firstprivate(p)
+  do i = 1, n
+    a(i) = p%x
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_derived
+! CHECK: acc.firstprivate varPtr({{.*}}) recipe({{.*}}) name("p")
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop combined(parallel)
+
+! Pointer and allocatable scalars are boxed, so they stay compute-only.
+subroutine acc_parallel_loop_firstprivate_pointer
+  integer, pointer :: p
+  integer, target :: t
+  integer :: i, n
+  real :: a(10)
+  n = 10
+  t = 7
+  p => t
+  !$acc parallel loop firstprivate(p)
+  do i = 1, n
+    a(i) = p
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_pointer
+! CHECK: %[[FP_P:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<!fir.box<!fir.ptr<i32>>>) recipe({{.*}}) name("p") -> !fir.ref<!fir.box<!fir.ptr<i32>>>
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_P]] : !fir.ref<!fir.box<!fir.ptr<i32>>>)
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop combined(parallel)
+
+subroutine acc_parallel_loop_firstprivate_allocatable
+  integer, allocatable :: v
+  integer :: i, n
+  real :: a(10)
+  n = 10
+  allocate(v)
+  v = 7
+  !$acc parallel loop firstprivate(v)
+  do i = 1, n
+    a(i) = v
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_allocatable
+! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<!fir.box<!fir.heap<i32>>>) recipe({{.*}}) name("v") -> !fir.ref<!fir.box<!fir.heap<i32>>>
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<!fir.box<!fir.heap<i32>>>)
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop combined(parallel)
+
+subroutine acc_parallel_loop_firstprivate_section
+  integer :: i, n
+  real :: b(10)
+  n = 10
+  !$acc parallel loop firstprivate(b(1:5))
+  do i = 1, n
+    b(1) = i
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_section
+! CHECK: %[[FP_B:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<!fir.array<10xf32>>) bounds({{.*}}) recipe({{.*}}) name("b(1:5)") -> !fir.ref<!fir.array<10xf32>>
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_B]] : !fir.ref<!fir.array<10xf32>>)
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop combined(parallel)
+
+! Non-combined parallel with a single loop is left to a later pass.
+subroutine acc_parallel_firstprivate_one_loop
+  integer :: i, n, v
+  real :: a(10)
+  n = 10
+  v = 7
+  !$acc parallel firstprivate(v)
+  !$acc loop
+  do i = 1, n
+    a(i) = v
+  end do
+  !$acc end parallel
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_firstprivate_one_loop
+! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
+! CHECK: acc.parallel {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop
diff --git a/flang/test/Lower/OpenACC/acc-private.f90 b/flang/test/Lower/OpenACC/acc-private.f90
index aa5ebc5e3a5d2..5876a4cff7b1e 100644
--- a/flang/test/Lower/OpenACC/acc-private.f90
+++ b/flang/test/Lower/OpenACC/acc-private.f90
@@ -436,6 +436,7 @@ program acc_private
 
 ! CHECK: %[[FP_B:.*]] = acc.firstprivate varPtr(%[[DECLB]]#0 : !fir.ref<!fir.array<100xf32>>) recipe(@firstprivatization_ref_100xf32) name("b") -> !fir.ref<!fir.array<100xf32>>
 ! CHECK: acc.parallel {{.*}} firstprivate(%[[FP_B]] : !fir.ref<!fir.array<100xf32>>)
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
 ! CHECK: acc.yield
 
   !$acc parallel loop firstprivate(b(51:100))
@@ -450,6 +451,8 @@ program acc_private
 ! CHECK: %[[BOUND:.*]] = acc.bounds lowerbound(%[[LB]] : index) upperbound(%[[UB]] : index) extent(%{{.*}} : index) stride(%[[C1]] : index) startIdx(%[[C1]] : index)
 ! CHECK: %[[FP_B:.*]] = acc.firstprivate varPtr(%[[DECLB]]#0 : !fir.ref<!fir.array<100xf32>>) bounds(%[[BOUND]]) recipe(@firstprivatization_section_lb50.ub99_ref_100xf32) name("b(51:100)") -> !fir.ref<!fir.array<100xf32>>
 ! CHECK: acc.parallel {{.*}} firstprivate(%[[FP_B]] : !fir.ref<!fir.array<100xf32>>)
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop
 
 end program
 
@@ -563,6 +566,8 @@ subroutine acc_firstprivate_assumed_shape(a, n)
 ! CHECK-LABEL: func.func @_QPacc_firstprivate_assumed_shape
 ! CHECK: %[[FIRSTPRIVATE_A:.*]] = acc.firstprivate var(%{{.*}} : !fir.box<!fir.array<?xi32>>) recipe(@firstprivatization_box_Uxi32) name("a") -> !fir.box<!fir.array<?xi32>>
 ! CHECK: acc.parallel {{.*}}firstprivate(%[[FIRSTPRIVATE_A]] : !fir.box<!fir.array<?xi32>>) {
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop
 
 subroutine acc_firstprivate_assumed_shape_with_section(a, n)
   integer :: a(:), i, n
@@ -576,6 +581,8 @@ subroutine acc_firstprivate_assumed_shape_with_section(a, n)
 ! CHECK-LABEL: func.func @_QPacc_firstprivate_assumed_shape_with_section
 ! CHECK: %[[FIRSTPRIVATE_A:.*]] = acc.firstprivate var(%{{.*}} : !fir.box<!fir.array<?xi32>>) bounds(%{{.*}}) recipe(@firstprivatization_section_lb4.ub9_box_Uxi32) name("a(5:10)") -> !fir.box<!fir.array<?xi32>>
 ! CHECK: acc.parallel {{.*}}firstprivate(%[[FIRSTPRIVATE_A]] : !fir.box<!fir.array<?xi32>>)
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop
 
 subroutine acc_firstprivate_dynamic_extent(a, n)
   integer :: n, i
@@ -590,6 +597,8 @@ subroutine acc_firstprivate_dynamic_extent(a, n)
 ! CHECK-LABEL: func.func @_QPacc_firstprivate_dynamic_extent
 ! CHECK: %[[FIRSTPRIVATE_A:.*]] = acc.firstprivate var(%{{.*}} : !fir.box<!fir.array<?x?x2xi32>>) recipe(@firstprivatization_box_UxUx2xi32) name("a") -> !fir.box<!fir.array<?x?x2xi32>>
 ! CHECK: acc.parallel {{.*}}firstprivate(%[[FIRSTPRIVATE_A]] : !fir.box<!fir.array<?x?x2xi32>>)
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop
 
 module acc_declare_equivalent
   integer, parameter :: n = 10
diff --git a/flang/test/Lower/OpenACC/acc-serial-loop.f90 b/flang/test/Lower/OpenACC/acc-serial-loop.f90
index 703cfe79ee28e..2a2054f6fb805 100644
--- a/flang/test/Lower/OpenACC/acc-serial-loop.f90
+++ b/flang/test/Lower/OpenACC/acc-serial-loop.f90
@@ -620,3 +620,22 @@ subroutine acc_serial_loop_firstprivate_scalar
 ! CHECK: acc.serial combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
 ! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
 ! CHECK: acc.loop combined(serial)
+
+! serial loop is not combined parallel, even with independent.
+subroutine acc_serial_loop_firstprivate_independent
+  integer :: i, n, v
+  real :: a(10)
+  n = 10
+  v = 7
+  !$acc serial loop independent firstprivate(v)
+  do i = 1, n
+    a(i) = v
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_serial_loop_firstprivate_independent
+! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
+! CHECK: acc.serial combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop combined(serial)
+! CHECK: } inclusiveUpperbound(array<i1: true>) independent

>From d679b1662b5498a786cc2cd2ae1263cb6312e1b9 Mon Sep 17 00:00:00 2001
From: Susan Tan <zujunt at nvidia.com>
Date: Thu, 10 Sep 2026 14:13:45 -0700
Subject: [PATCH 03/13] tweak

---
 flang/test/Lower/OpenACC/acc-parallel-loop.f90 | 1 -
 1 file changed, 1 deletion(-)

diff --git a/flang/test/Lower/OpenACC/acc-parallel-loop.f90 b/flang/test/Lower/OpenACC/acc-parallel-loop.f90
index 8c98e17628b48..cd3717bffccb9 100644
--- a/flang/test/Lower/OpenACC/acc-parallel-loop.f90
+++ b/flang/test/Lower/OpenACC/acc-parallel-loop.f90
@@ -453,7 +453,6 @@ subroutine acc_parallel_loop
 
 ! CHECK:      %[[ACC_PRIVATE_B:.*]] = acc.firstprivate varPtr(%[[DECLB]]#0 : !fir.ref<!fir.array<10xf32>>) recipe(@firstprivatization_ref_10xf32) name("b") -> !fir.ref<!fir.array<10xf32>>
 ! CHECK:      acc.parallel {{.*}} firstprivate(%[[ACC_PRIVATE_B]] : !fir.ref<!fir.array<10xf32>>) {
-! CHECK-NOT:    acc.firstprivate {{.*}} implicit(true)
 ! CHECK:        %[[ACC_PRIVATE_A:.*]] = acc.private varPtr(%[[DECLA]]#0 : !fir.ref<!fir.array<10xf32>>) recipe(@privatization_ref_10xf32) name("a") -> !fir.ref<!fir.array<10xf32>>
 ! CHECK:        acc.loop {{.*}} private(%[[ACC_PRIVATE_A]]{{.*}} : !fir.ref<!fir.array<10xf32>>{{.*}})
 ! CHECK-NOT:      fir.do_loop

>From 14768db9df03ad2c6b22c66c67cbbfba69af7b10 Mon Sep 17 00:00:00 2001
From: Susan Tan <zujunt at nvidia.com>
Date: Thu, 10 Sep 2026 14:15:43 -0700
Subject: [PATCH 04/13] tweak

---
 flang/test/Lower/OpenACC/acc-private.f90 | 9 ---------
 1 file changed, 9 deletions(-)

diff --git a/flang/test/Lower/OpenACC/acc-private.f90 b/flang/test/Lower/OpenACC/acc-private.f90
index 5876a4cff7b1e..aa5ebc5e3a5d2 100644
--- a/flang/test/Lower/OpenACC/acc-private.f90
+++ b/flang/test/Lower/OpenACC/acc-private.f90
@@ -436,7 +436,6 @@ program acc_private
 
 ! CHECK: %[[FP_B:.*]] = acc.firstprivate varPtr(%[[DECLB]]#0 : !fir.ref<!fir.array<100xf32>>) recipe(@firstprivatization_ref_100xf32) name("b") -> !fir.ref<!fir.array<100xf32>>
 ! CHECK: acc.parallel {{.*}} firstprivate(%[[FP_B]] : !fir.ref<!fir.array<100xf32>>)
-! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
 ! CHECK: acc.yield
 
   !$acc parallel loop firstprivate(b(51:100))
@@ -451,8 +450,6 @@ program acc_private
 ! CHECK: %[[BOUND:.*]] = acc.bounds lowerbound(%[[LB]] : index) upperbound(%[[UB]] : index) extent(%{{.*}} : index) stride(%[[C1]] : index) startIdx(%[[C1]] : index)
 ! CHECK: %[[FP_B:.*]] = acc.firstprivate varPtr(%[[DECLB]]#0 : !fir.ref<!fir.array<100xf32>>) bounds(%[[BOUND]]) recipe(@firstprivatization_section_lb50.ub99_ref_100xf32) name("b(51:100)") -> !fir.ref<!fir.array<100xf32>>
 ! CHECK: acc.parallel {{.*}} firstprivate(%[[FP_B]] : !fir.ref<!fir.array<100xf32>>)
-! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
-! CHECK: acc.loop
 
 end program
 
@@ -566,8 +563,6 @@ subroutine acc_firstprivate_assumed_shape(a, n)
 ! CHECK-LABEL: func.func @_QPacc_firstprivate_assumed_shape
 ! CHECK: %[[FIRSTPRIVATE_A:.*]] = acc.firstprivate var(%{{.*}} : !fir.box<!fir.array<?xi32>>) recipe(@firstprivatization_box_Uxi32) name("a") -> !fir.box<!fir.array<?xi32>>
 ! CHECK: acc.parallel {{.*}}firstprivate(%[[FIRSTPRIVATE_A]] : !fir.box<!fir.array<?xi32>>) {
-! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
-! CHECK: acc.loop
 
 subroutine acc_firstprivate_assumed_shape_with_section(a, n)
   integer :: a(:), i, n
@@ -581,8 +576,6 @@ subroutine acc_firstprivate_assumed_shape_with_section(a, n)
 ! CHECK-LABEL: func.func @_QPacc_firstprivate_assumed_shape_with_section
 ! CHECK: %[[FIRSTPRIVATE_A:.*]] = acc.firstprivate var(%{{.*}} : !fir.box<!fir.array<?xi32>>) bounds(%{{.*}}) recipe(@firstprivatization_section_lb4.ub9_box_Uxi32) name("a(5:10)") -> !fir.box<!fir.array<?xi32>>
 ! CHECK: acc.parallel {{.*}}firstprivate(%[[FIRSTPRIVATE_A]] : !fir.box<!fir.array<?xi32>>)
-! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
-! CHECK: acc.loop
 
 subroutine acc_firstprivate_dynamic_extent(a, n)
   integer :: n, i
@@ -597,8 +590,6 @@ subroutine acc_firstprivate_dynamic_extent(a, n)
 ! CHECK-LABEL: func.func @_QPacc_firstprivate_dynamic_extent
 ! CHECK: %[[FIRSTPRIVATE_A:.*]] = acc.firstprivate var(%{{.*}} : !fir.box<!fir.array<?x?x2xi32>>) recipe(@firstprivatization_box_UxUx2xi32) name("a") -> !fir.box<!fir.array<?x?x2xi32>>
 ! CHECK: acc.parallel {{.*}}firstprivate(%[[FIRSTPRIVATE_A]] : !fir.box<!fir.array<?x?x2xi32>>)
-! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
-! CHECK: acc.loop
 
 module acc_declare_equivalent
   integer, parameter :: n = 10

>From 6468a19366fd80859636c89da37689dc755761c2 Mon Sep 17 00:00:00 2001
From: Susan Tan <zujunt at nvidia.com>
Date: Thu, 10 Sep 2026 14:51:54 -0700
Subject: [PATCH 05/13] add implicit tests

---
 .../test/Lower/OpenACC/acc-parallel-loop.f90  | 36 +++++++++++++++++++
 1 file changed, 36 insertions(+)

diff --git a/flang/test/Lower/OpenACC/acc-parallel-loop.f90 b/flang/test/Lower/OpenACC/acc-parallel-loop.f90
index cd3717bffccb9..54f7be328dd5f 100644
--- a/flang/test/Lower/OpenACC/acc-parallel-loop.f90
+++ b/flang/test/Lower/OpenACC/acc-parallel-loop.f90
@@ -771,6 +771,42 @@ subroutine acc_parallel_firstprivate_sibling_loops
 ! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
 ! CHECK: acc.loop
 
+! Scalars used without an explicit firstprivate are implicitly firstprivate
+! on the compute construct later. Do not emit a loop firstprivate here.
+subroutine acc_parallel_loop_no_explicit_firstprivate
+  integer :: i, n, v
+  real :: a(10)
+  n = 10
+  v = 7
+  !$acc parallel loop
+  do i = 1, n
+    a(i) = v
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_no_explicit_firstprivate
+! CHECK: acc.parallel combined(loop) {
+! CHECK-NOT: acc.firstprivate
+! CHECK: acc.loop combined(parallel)
+
+! copyin is explicit but not firstprivate, so the loop still gets no copy.
+subroutine acc_parallel_loop_copyin_scalar
+  integer :: i, n, v
+  real :: a(10)
+  n = 10
+  v = 7
+  !$acc parallel loop copyin(v)
+  do i = 1, n
+    a(i) = v
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_copyin_scalar
+! CHECK: acc.copyin {{.*}} name("v")
+! CHECK: acc.parallel combined(loop) {{.*}}dataOperands
+! CHECK-NOT: acc.firstprivate
+! CHECK: acc.loop combined(parallel)
+
 ! Explicit independent is the same as the parallel loop default.
 subroutine acc_parallel_loop_firstprivate_independent
   integer :: i, n, v

>From 958856b53b334f4e626bf979052aa26b056526e3 Mon Sep 17 00:00:00 2001
From: Susan Tan <zujunt at nvidia.com>
Date: Thu, 10 Sep 2026 14:56:13 -0700
Subject: [PATCH 06/13] add description

---
 flang/docs/OpenACC-extensions.md | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/flang/docs/OpenACC-extensions.md b/flang/docs/OpenACC-extensions.md
index c22f00efec758..41fa1513e33cb 100644
--- a/flang/docs/OpenACC-extensions.md
+++ b/flang/docs/OpenACC-extensions.md
@@ -57,6 +57,18 @@ These extensions require no flag.
   this use (`-Wportability`, also enabled by `-pedantic`; suppress with
   `-Wno-portability`).
 
+### Combined `parallel loop` firstprivate
+
+`firstprivate` is a compute-construct clause, not a `loop` clause. On a
+combined `parallel loop`, Flang keeps the explicit clause on the compute
+construct and also attaches an implicit `firstprivate` on the associated
+`acc.loop` so independent iterations do not share that copy.
+
+This applies only to OpenACC scalars on an independent combined
+`parallel loop`. It is not applied for `serial loop`, `kernels loop`,
+standalone `acc loop`, `seq`/`auto`, non-scalars, nested or collapsed inner
+loops, or scalars that are only implicitly firstprivate.
+
 ## Extensions enabled by default
 
 ### `-fopenacc-multiple-names-in-routine` — `!$acc routine(<name>[, <name>]*) <clause-list>`

>From c95375574457ec63821d0fb37ee547b768fb35d3 Mon Sep 17 00:00:00 2001
From: Susan Tan <zujunt at nvidia.com>
Date: Mon, 14 Sep 2026 09:13:39 -0700
Subject: [PATCH 07/13] add option

---
 clang/include/clang/Options/FlangOptions.td   |  3 +
 flang/docs/OpenACC-extensions.md              | 25 ++++----
 flang/include/flang/Lower/LoweringOptions.def |  5 ++
 flang/lib/Frontend/CompilerInvocation.cpp     |  6 ++
 flang/lib/Lower/OpenACC.cpp                   | 60 ++++++++-----------
 ...tprivate-derived-allocatable-component.f90 |  6 +-
 .../acc-firstprivate-derived-user-assign.f90  |  6 +-
 .../OpenACC/acc-firstprivate-derived.f90      |  6 +-
 .../test/Lower/OpenACC/acc-parallel-loop.f90  | 43 +++++++------
 flang/test/Lower/OpenACC/acc-serial-loop.f90  | 13 ++--
 10 files changed, 94 insertions(+), 79 deletions(-)

diff --git a/clang/include/clang/Options/FlangOptions.td b/clang/include/clang/Options/FlangOptions.td
index 8fd0d78a2659c..97c5cb3dbf007 100644
--- a/clang/include/clang/Options/FlangOptions.td
+++ b/clang/include/clang/Options/FlangOptions.td
@@ -199,6 +199,9 @@ defm openacc_default_none_scalars_strict : OptOutFC1FFlag<"openacc-default-none-
 defm openacc_multiple_names_in_routine : OptOutFC1FFlag<"openacc-multiple-names-in-routine",
   "Accept multiple names in OpenACC ROUTINE directive (extension)",
   "Do not accept multiple names in OpenACC ROUTINE directive">;
+defm openacc_combined_loop_firstprivate : OptOutFC1FFlag<"openacc-combined-loop-firstprivate",
+  "Attach firstprivate on combined loop in addition to compute construct (extension)",
+  "Keep firstprivate only on the compute construct for combined loop">;
 defm prefer_intrinsic_module_use_association : OptOutFC1FFlag<"prefer-intrinsic-module-use-association",
   "Resolve a USE association conflict in favor of an intrinsic module generic (extension)",
   "Diagnose a USE association conflict with an intrinsic module generic">;
diff --git a/flang/docs/OpenACC-extensions.md b/flang/docs/OpenACC-extensions.md
index 41fa1513e33cb..9816db6dbc464 100644
--- a/flang/docs/OpenACC-extensions.md
+++ b/flang/docs/OpenACC-extensions.md
@@ -57,19 +57,24 @@ These extensions require no flag.
   this use (`-Wportability`, also enabled by `-pedantic`; suppress with
   `-Wno-portability`).
 
-### Combined `parallel loop` firstprivate
+## Extensions enabled by default
 
-`firstprivate` is a compute-construct clause, not a `loop` clause. On a
-combined `parallel loop`, Flang keeps the explicit clause on the compute
-construct and also attaches an implicit `firstprivate` on the associated
-`acc.loop` so independent iterations do not share that copy.
+### `-fopenacc-combined-loop-firstprivate` — combined loop firstprivate
 
-This applies only to OpenACC scalars on an independent combined
-`parallel loop`. It is not applied for `serial loop`, `kernels loop`,
-standalone `acc loop`, `seq`/`auto`, non-scalars, nested or collapsed inner
-loops, or scalars that are only implicitly firstprivate.
+`firstprivate` is a compute-construct clause, not a `loop` clause.  On a
+combined `parallel loop` or `serial loop`, Flang keeps the explicit clause on
+the compute construct and also attaches an implicit `firstprivate` on the
+associated `acc.loop` so each thread gets its own initialized copy.
 
-## Extensions enabled by default
+This applies to all types (scalars, arrays, derived types, etc.) and all
+parallelism modes (`independent`, `seq`, `auto`), consistent with how `private`
+and `reduction` are handled on combined constructs.  `kernels loop` is not
+affected because `kernels` cannot take `firstprivate`.  Standalone `acc loop`
+and non-combined `parallel` / `serial` with separate inner `acc loop` are not
+affected.
+
+Disable with `-fno-openacc-combined-loop-firstprivate` to keep firstprivate
+only on the compute construct (spec behavior).
 
 ### `-fopenacc-multiple-names-in-routine` — `!$acc routine(<name>[, <name>]*) <clause-list>`
 
diff --git a/flang/include/flang/Lower/LoweringOptions.def b/flang/include/flang/Lower/LoweringOptions.def
index 61ccb2ac19bdd..2931a5edc56e9 100644
--- a/flang/include/flang/Lower/LoweringOptions.def
+++ b/flang/include/flang/Lower/LoweringOptions.def
@@ -97,5 +97,10 @@ ENUM_LOWERINGOPT(FPMaxminBehavior, Fortran::common::FPMaxminBehavior, 2, 0)
 /// 0 means no trapping. Bit values match IEEE_FLAG_TYPE encoding.
 ENUM_LOWERINGOPT(FPExceptionTraps, unsigned, 8, 0)
 
+/// If true (default), attach an implicit firstprivate on the loop of a
+/// combined construct (parallel loop, serial loop) in addition to the compute
+/// construct. Controlled by -f[no-]openacc-combined-loop-firstprivate.
+ENUM_LOWERINGOPT(OpenACCCombinedLoopFirstprivate, unsigned, 1, 1)
+
 #undef LOWERINGOPT
 #undef ENUM_LOWERINGOPT
diff --git a/flang/lib/Frontend/CompilerInvocation.cpp b/flang/lib/Frontend/CompilerInvocation.cpp
index b76b38ff6497f..7189d9f06cd4a 100644
--- a/flang/lib/Frontend/CompilerInvocation.cpp
+++ b/flang/lib/Frontend/CompilerInvocation.cpp
@@ -1823,6 +1823,12 @@ bool CompilerInvocation::createFromArgs(
     invoc.loweringOpts.setRepackArraysWhole(arg->getValue() ==
                                             llvm::StringRef{"whole"});
 
+  // -f[no-]openacc-combined-loop-firstprivate
+  invoc.loweringOpts.setOpenACCCombinedLoopFirstprivate(args.hasFlag(
+      clang::options::OPT_fopenacc_combined_loop_firstprivate,
+      clang::options::OPT_fno_openacc_combined_loop_firstprivate,
+      /*default=*/true));
+
   if (auto *arg = args.getLastArg(clang::options::OPT_ffp_maxmin_behavior_EQ)) {
     auto value = Fortran::common::parseFPMaxminBehavior(arg->getValue());
     invoc.getCodeGenOpts().setFPMaxminBehavior(value);
diff --git a/flang/lib/Lower/OpenACC.cpp b/flang/lib/Lower/OpenACC.cpp
index fdca3c6f182df..5c23837d03471 100644
--- a/flang/lib/Lower/OpenACC.cpp
+++ b/flang/lib/Lower/OpenACC.cpp
@@ -1723,29 +1723,22 @@ loopWillBeIndependent(Fortran::lower::AbstractConverter &converter,
   }
 }
 
-// Attach an implicit firstprivate on this combined loop in addition to the
-// compute clause (firstprivate is not a loop clause in the spec) when:
-//  1. Combined `parallel loop` — the region is that one loop. Not `serial
-//     loop` (seq by default), `kernels loop`, or a standalone `acc loop`.
-//  2. Independent (the `parallel loop` default). Not `seq`/`auto`, which
-//     must keep a carried firstprivate.
-//  3. Scalar. Nested loops reuse this copy via remap; they get no clause.
+// Attach an implicit firstprivate on a combined loop in addition to the
+// compute clause.  On a combined construct, private and reduction already
+// apply to both the compute region and the loop; firstprivate should too.
+// Controlled by -f[no-]openacc-combined-loop-firstprivate (default on).
+// Applies to parallel loop and serial loop (kernels cannot take
+// firstprivate), all types (scalars, arrays, derived, etc.), and all
+// parallelism modes (independent, seq, auto).
 static bool shouldAttachFirstprivateOnCombinedLoop(
     Fortran::lower::AbstractConverter &converter,
-    const Fortran::parser::AccClauseList &accClauseList,
-    std::optional<mlir::acc::CombinedConstructsType> combinedConstructs,
-    const Fortran::parser::AccObject &accObject) {
-  if (!combinedConstructs ||
-      *combinedConstructs != mlir::acc::CombinedConstructsType::ParallelLoop)
+    std::optional<mlir::acc::CombinedConstructsType> combinedConstructs) {
+  if (!combinedConstructs)
     return false;
-  if (!loopWillBeIndependent(converter, accClauseList,
-                             llvm::acc::ACCD_parallel_loop))
+  if (*combinedConstructs != mlir::acc::CombinedConstructsType::ParallelLoop &&
+      *combinedConstructs != mlir::acc::CombinedConstructsType::SerialLoop)
     return false;
-  mlir::Value var =
-      converter.getSymbolAddress(getSymbolFromAccObject(accObject));
-  return var &&
-         mlir::acc::bitEnumContainsAny(mlir::acc::getTypeCategory(var),
-                                       mlir::acc::VariableTypeCategory::scalar);
+  return converter.getLoweringOptions().getOpenACCCombinedLoopFirstprivate();
 }
 
 // Helper to visit Bounds of DO LOOP nest.
@@ -2528,21 +2521,20 @@ static mlir::acc::LoopOp createLoopOp(
     } else if (const auto *firstprivateClause =
                    std::get_if<Fortran::parser::AccClause::Firstprivate>(
                        &clause.u)) {
-      // Duplicate scalar firstprivate onto this combined independent loop.
-      // The compute construct already has the user-facing firstprivate (host
-      // seed). After that remap, getSymbolAddress is the compute copy, so the
-      // loop clause's varPtr chains from it. implicit=true: firstprivate is
-      // not a loop clause in the spec.
-      genDataOperandOperations<mlir::acc::FirstprivateOp>(
-          firstprivateClause->v, converter, semanticsContext, stmtCtx,
-          firstprivateOperands, mlir::acc::DataClause::acc_firstprivate,
-          /*structured=*/true, /*implicit=*/true,
-          /*async=*/{}, /*asyncDeviceTypes=*/{}, /*asyncOnlyDeviceTypes=*/{},
-          /*setDeclareAttr=*/false, &dataMap,
-          /*filter=*/[&](const Fortran::parser::AccObject &obj) {
-            return shouldAttachFirstprivateOnCombinedLoop(
-                converter, accClauseList, combinedConstructs, obj);
-          });
+      // Duplicate firstprivate onto this combined loop.  The compute
+      // construct already has the user-facing firstprivate (host seed).
+      // After that remap, getSymbolAddress is the compute copy, so the
+      // loop clause's varPtr chains from it.  implicit=true: firstprivate
+      // is not a loop clause in the spec.
+      if (shouldAttachFirstprivateOnCombinedLoop(converter,
+                                                 combinedConstructs)) {
+        genDataOperandOperations<mlir::acc::FirstprivateOp>(
+            firstprivateClause->v, converter, semanticsContext, stmtCtx,
+            firstprivateOperands, mlir::acc::DataClause::acc_firstprivate,
+            /*structured=*/true, /*implicit=*/true,
+            /*async=*/{}, /*asyncDeviceTypes=*/{}, /*asyncOnlyDeviceTypes=*/{},
+            /*setDeclareAttr=*/false, &dataMap);
+      }
     } else if (const auto *reductionClause =
                    std::get_if<Fortran::parser::AccClause::Reduction>(
                        &clause.u)) {
diff --git a/flang/test/Lower/OpenACC/acc-firstprivate-derived-allocatable-component.f90 b/flang/test/Lower/OpenACC/acc-firstprivate-derived-allocatable-component.f90
index 7a71981d1c915..3403ce5cc25af 100644
--- a/flang/test/Lower/OpenACC/acc-firstprivate-derived-allocatable-component.f90
+++ b/flang/test/Lower/OpenACC/acc-firstprivate-derived-allocatable-component.f90
@@ -51,12 +51,14 @@ subroutine test(a)
 ! CHECK:             %[[VAL_9:.*]] = arith.constant 1 : i32
 ! CHECK:             %[[VAL_10:.*]] = fir.load %[[VAL_5]]#0 : !fir.ref<i32>
 ! CHECK:             %[[VAL_11:.*]] = arith.constant 1 : i32
+! CHECK:             %[[FP_A_LOOP:.*]] = acc.firstprivate varPtr({{.*}}) recipe(@firstprivatization_ref_rec__QMm_firstprivate_derived_alloc_compTpoint) implicit(true) name("a")
 ! CHECK:             %[[VAL_12:.*]] = acc.private varPtr(%[[VAL_3]]#0 : !fir.ref<i32>) recipe(@privatization_ref_i32) implicit(true) name("i") -> !fir.ref<i32>
-! CHECK:             acc.loop combined(parallel) private(%[[VAL_12]] : !fir.ref<i32>) control(%[[VAL_14:.*]] : i32) = (%[[VAL_9]] : i32) to (%[[VAL_10]] : i32)  step (%[[VAL_11]] : i32) {
+! CHECK:             acc.loop combined(parallel) firstprivate(%[[FP_A_LOOP]] : {{.*}}) private(%[[VAL_12]] : !fir.ref<i32>) control(%[[VAL_14:.*]] : i32) = (%[[VAL_9]] : i32) to (%[[VAL_10]] : i32)  step (%[[VAL_11]] : i32) {
+! CHECK:               %[[VAL_8B:.*]]:2 = hlfir.declare %[[FP_A_LOOP]]
 ! CHECK:               %[[VAL_13:.*]]:2 = hlfir.declare %[[VAL_12]] {uniq_name = "_QMm_firstprivate_derived_alloc_compFtestEi"} : (!fir.ref<i32>) -> (!fir.ref<i32>, !fir.ref<i32>)
 ! CHECK:               fir.store %[[VAL_14]] to %[[VAL_13]]#0 : !fir.ref<i32>
 ! CHECK:               %[[VAL_15:.*]] = arith.constant 1.000000e+00 : f32
-! CHECK:               %[[VAL_16:.*]] = hlfir.designate %[[VAL_8]]#0{"x"}   {fortran_attrs = #fir.var_attrs<allocatable>} : (!fir.ref<!fir.type<_QMm_firstprivate_derived_alloc_compTpoint{x:!fir.box<!fir.heap<!fir.array<?xf32>>>}>>) -> !fir.ref<!fir.box<!fir.heap<!fir.array<?xf32>>>>
+! CHECK:               %[[VAL_16:.*]] = hlfir.designate %[[VAL_8B]]#0{"x"}   {fortran_attrs = #fir.var_attrs<allocatable>} : (!fir.ref<!fir.type<_QMm_firstprivate_derived_alloc_compTpoint{x:!fir.box<!fir.heap<!fir.array<?xf32>>>}>>) -> !fir.ref<!fir.box<!fir.heap<!fir.array<?xf32>>>>
 ! CHECK:               %[[VAL_17:.*]] = fir.load %[[VAL_16]] : !fir.ref<!fir.box<!fir.heap<!fir.array<?xf32>>>>
 ! CHECK:               %[[VAL_18:.*]] = arith.constant 10 : index
 ! CHECK:               %[[VAL_19:.*]] = hlfir.designate %[[VAL_17]] (%[[VAL_18]])  : (!fir.box<!fir.heap<!fir.array<?xf32>>>, index) -> !fir.ref<f32>
diff --git a/flang/test/Lower/OpenACC/acc-firstprivate-derived-user-assign.f90 b/flang/test/Lower/OpenACC/acc-firstprivate-derived-user-assign.f90
index f92c2a5ad680a..edf7db534baa9 100644
--- a/flang/test/Lower/OpenACC/acc-firstprivate-derived-user-assign.f90
+++ b/flang/test/Lower/OpenACC/acc-firstprivate-derived-user-assign.f90
@@ -55,15 +55,17 @@ subroutine test()
 ! CHECK:           %[[VAL_7:.*]] = acc.firstprivate varPtr(%[[VAL_2]]#0 : !fir.ref<!fir.type<_QMm_firstprivate_derived_user_defTpoint{x:f32,y:f32,z:f32}>>) recipe(@firstprivatization_ref_rec__QMm_firstprivate_derived_user_defTpoint) name("a") -> !fir.ref<!fir.type<_QMm_firstprivate_derived_user_defTpoint{x:f32,y:f32,z:f32}>>
 ! CHECK:           acc.parallel combined(loop) firstprivate(%[[VAL_7]] : !fir.ref<!fir.type<_QMm_firstprivate_derived_user_defTpoint{x:f32,y:f32,z:f32}>>) {
 ! CHECK:             %[[VAL_8:.*]]:2 = hlfir.declare %[[VAL_7]] {uniq_name = "_QMm_firstprivate_derived_user_defFtestEa"} : (!fir.ref<!fir.type<_QMm_firstprivate_derived_user_defTpoint{x:f32,y:f32,z:f32}>>) -> (!fir.ref<!fir.type<_QMm_firstprivate_derived_user_defTpoint{x:f32,y:f32,z:f32}>>, !fir.ref<!fir.type<_QMm_firstprivate_derived_user_defTpoint{x:f32,y:f32,z:f32}>>)
+! CHECK:             %[[FP_A_LOOP:.*]] = acc.firstprivate varPtr(%[[VAL_8]]#0 : !fir.ref<!fir.type<_QMm_firstprivate_derived_user_defTpoint{x:f32,y:f32,z:f32}>>) recipe(@firstprivatization_ref_rec__QMm_firstprivate_derived_user_defTpoint) implicit(true) name("a") -> !fir.ref<!fir.type<_QMm_firstprivate_derived_user_defTpoint{x:f32,y:f32,z:f32}>>
 ! CHECK:             %[[VAL_9:.*]] = arith.constant 1 : i32
 ! CHECK:             %[[VAL_10:.*]] = fir.load %[[VAL_6]]#0 : !fir.ref<i32>
 ! CHECK:             %[[VAL_11:.*]] = arith.constant 1 : i32
 ! CHECK:             %[[VAL_12:.*]] = acc.private varPtr(%[[VAL_4]]#0 : !fir.ref<i32>) recipe(@privatization_ref_i32) implicit(true) name("i") -> !fir.ref<i32>
-! CHECK:             acc.loop combined(parallel) private(%[[VAL_12]] : !fir.ref<i32>) control(%[[VAL_14:.*]] : i32) = (%[[VAL_9]] : i32) to (%[[VAL_10]] : i32)  step (%[[VAL_11]] : i32) {
+! CHECK:             acc.loop combined(parallel) private(%[[VAL_12]] : !fir.ref<i32>) firstprivate(%[[FP_A_LOOP]] : !fir.ref<!fir.type<_QMm_firstprivate_derived_user_defTpoint{x:f32,y:f32,z:f32}>>) control(%[[VAL_14:.*]] : i32) = (%[[VAL_9]] : i32) to (%[[VAL_10]] : i32)  step (%[[VAL_11]] : i32) {
+! CHECK:               %[[VAL_8B:.*]]:2 = hlfir.declare %[[FP_A_LOOP]] {uniq_name = "_QMm_firstprivate_derived_user_defFtestEa"}
   ! CHECK:             %[[VAL_13:.*]]:2 = hlfir.declare %[[VAL_12]] {uniq_name = "_QMm_firstprivate_derived_user_defFtestEi"} : (!fir.ref<i32>) -> (!fir.ref<i32>, !fir.ref<i32>)
 ! CHECK:               fir.store %[[VAL_14]] to %[[VAL_13]]#0 : !fir.ref<i32>
 ! CHECK:               %[[VAL_15:.*]] = arith.constant 1.000000e+00 : f32
-! CHECK:               %[[VAL_16:.*]] = hlfir.designate %[[VAL_8]]#0{"x"}   : (!fir.ref<!fir.type<_QMm_firstprivate_derived_user_defTpoint{x:f32,y:f32,z:f32}>>) -> !fir.ref<f32>
+! CHECK:               %[[VAL_16:.*]] = hlfir.designate %[[VAL_8B]]#0{"x"}   : (!fir.ref<!fir.type<_QMm_firstprivate_derived_user_defTpoint{x:f32,y:f32,z:f32}>>) -> !fir.ref<f32>
 ! CHECK:               hlfir.assign %[[VAL_15]] to %[[VAL_16]] : f32, !fir.ref<f32>
 ! CHECK:               acc.yield
 ! CHECK:             } inclusiveUpperbound(array<i1: true>) independent
diff --git a/flang/test/Lower/OpenACC/acc-firstprivate-derived.f90 b/flang/test/Lower/OpenACC/acc-firstprivate-derived.f90
index ffc24a07e757c..8aa8b3156f3b1 100644
--- a/flang/test/Lower/OpenACC/acc-firstprivate-derived.f90
+++ b/flang/test/Lower/OpenACC/acc-firstprivate-derived.f90
@@ -41,15 +41,17 @@ subroutine test()
 ! CHECK:           %[[VAL_7:.*]] = acc.firstprivate varPtr(%[[VAL_2]]#0 : !fir.ref<!fir.type<_QMm_firstprivate_derivedTpoint{x:f32,y:f32,z:f32}>>) recipe(@firstprivatization_ref_rec__QMm_firstprivate_derivedTpoint) name("a") -> !fir.ref<!fir.type<_QMm_firstprivate_derivedTpoint{x:f32,y:f32,z:f32}>>
 ! CHECK:           acc.parallel combined(loop) firstprivate(%[[VAL_7]] : !fir.ref<!fir.type<_QMm_firstprivate_derivedTpoint{x:f32,y:f32,z:f32}>>) {
 ! CHECK:             %[[VAL_8:.*]]:2 = hlfir.declare %[[VAL_7]] {uniq_name = "_QMm_firstprivate_derivedFtestEa"} : (!fir.ref<!fir.type<_QMm_firstprivate_derivedTpoint{x:f32,y:f32,z:f32}>>) -> (!fir.ref<!fir.type<_QMm_firstprivate_derivedTpoint{x:f32,y:f32,z:f32}>>, !fir.ref<!fir.type<_QMm_firstprivate_derivedTpoint{x:f32,y:f32,z:f32}>>)
+! CHECK:             %[[FP_A_LOOP:.*]] = acc.firstprivate varPtr(%[[VAL_8]]#0 : !fir.ref<!fir.type<_QMm_firstprivate_derivedTpoint{x:f32,y:f32,z:f32}>>) recipe(@firstprivatization_ref_rec__QMm_firstprivate_derivedTpoint) implicit(true) name("a") -> !fir.ref<!fir.type<_QMm_firstprivate_derivedTpoint{x:f32,y:f32,z:f32}>>
 ! CHECK:             %[[VAL_9:.*]] = arith.constant 1 : i32
 ! CHECK:             %[[VAL_10:.*]] = fir.load %[[VAL_6]]#0 : !fir.ref<i32>
 ! CHECK:             %[[VAL_11:.*]] = arith.constant 1 : i32
 ! CHECK:             %[[VAL_12:.*]] = acc.private varPtr(%[[VAL_4]]#0 : !fir.ref<i32>) recipe(@privatization_ref_i32) implicit(true) name("i") -> !fir.ref<i32>
-! CHECK:             acc.loop combined(parallel) private(%[[VAL_12]] : !fir.ref<i32>) control(%[[VAL_14:.*]] : i32) = (%[[VAL_9]] : i32) to (%[[VAL_10]] : i32)  step (%[[VAL_11]] : i32) {
+! CHECK:             acc.loop combined(parallel) private(%[[VAL_12]] : !fir.ref<i32>) firstprivate(%[[FP_A_LOOP]] : !fir.ref<!fir.type<_QMm_firstprivate_derivedTpoint{x:f32,y:f32,z:f32}>>) control(%[[VAL_14:.*]] : i32) = (%[[VAL_9]] : i32) to (%[[VAL_10]] : i32)  step (%[[VAL_11]] : i32) {
+! CHECK:               %[[VAL_8B:.*]]:2 = hlfir.declare %[[FP_A_LOOP]] {uniq_name = "_QMm_firstprivate_derivedFtestEa"}
   ! CHECK:             %[[VAL_13:.*]]:2 = hlfir.declare %[[VAL_12]] {uniq_name = "_QMm_firstprivate_derivedFtestEi"} : (!fir.ref<i32>) -> (!fir.ref<i32>, !fir.ref<i32>)
 ! CHECK:               fir.store %[[VAL_14]] to %[[VAL_13]]#0 : !fir.ref<i32>
 ! CHECK:               %[[VAL_15:.*]] = arith.constant 1.000000e+00 : f32
-! CHECK:               %[[VAL_16:.*]] = hlfir.designate %[[VAL_8]]#0{"x"}   : (!fir.ref<!fir.type<_QMm_firstprivate_derivedTpoint{x:f32,y:f32,z:f32}>>) -> !fir.ref<f32>
+! CHECK:               %[[VAL_16:.*]] = hlfir.designate %[[VAL_8B]]#0{"x"}   : (!fir.ref<!fir.type<_QMm_firstprivate_derivedTpoint{x:f32,y:f32,z:f32}>>) -> !fir.ref<f32>
 ! CHECK:               hlfir.assign %[[VAL_15]] to %[[VAL_16]] : f32, !fir.ref<f32>
 ! CHECK:               acc.yield
 ! CHECK:             } inclusiveUpperbound(array<i1: true>) independent
diff --git a/flang/test/Lower/OpenACC/acc-parallel-loop.f90 b/flang/test/Lower/OpenACC/acc-parallel-loop.f90
index 54f7be328dd5f..5ed965bf802d5 100644
--- a/flang/test/Lower/OpenACC/acc-parallel-loop.f90
+++ b/flang/test/Lower/OpenACC/acc-parallel-loop.f90
@@ -672,9 +672,8 @@ subroutine acc_parallel_loop
 
 end subroutine acc_parallel_loop
 
-! Combined parallel loop firstprivate: scalar copies are also attached on the
-! independent acc.loop. Arrays, sections, derived/character/boxed types, seq
-! and auto loops, and non-combined loops are not.
+! Combined parallel loop firstprivate: copies are also attached on the
+! acc.loop, consistent with private and reduction on combined constructs.
 subroutine acc_parallel_loop_firstprivate_scalar
   integer :: i, n, v
   real :: a(10)
@@ -707,8 +706,8 @@ subroutine acc_parallel_loop_firstprivate_seq
 ! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_seq
 ! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
 ! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
-! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
-! CHECK: acc.loop combined(parallel)
+! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) implicit(true) name("v") -> !fir.ref<i32>
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<i32>)
 ! CHECK: } inclusiveUpperbound(array<i1: true>) seq
 
 subroutine acc_parallel_loop_firstprivate_auto
@@ -725,8 +724,8 @@ subroutine acc_parallel_loop_firstprivate_auto
 ! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_auto
 ! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
 ! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
-! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
-! CHECK: acc.loop combined(parallel)
+! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) implicit(true) name("v") -> !fir.ref<i32>
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<i32>)
 ! CHECK: } inclusiveUpperbound(array<i1: true>) auto_
 
 subroutine acc_parallel_loop_firstprivate_mixed
@@ -745,8 +744,8 @@ subroutine acc_parallel_loop_firstprivate_mixed
 ! CHECK: %[[FP_B:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<!fir.array<10xf32>>) recipe({{.*}}) name("b") -> !fir.ref<!fir.array<10xf32>>
 ! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_V]], %[[FP_B]] : !fir.ref<i32>, !fir.ref<!fir.array<10xf32>>)
 ! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) implicit(true) name("v") -> !fir.ref<i32>
-! CHECK-NOT: implicit(true) name("b")
-! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<i32>)
+! CHECK: %[[FP_B_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<!fir.array<10xf32>>) recipe({{.*}}) implicit(true) name("b") -> !fir.ref<!fir.array<10xf32>>
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_V_LOOP]], %[[FP_B_LOOP]] : !fir.ref<i32>, !fir.ref<!fir.array<10xf32>>)
 
 subroutine acc_parallel_firstprivate_sibling_loops
   integer :: i, j, n, v
@@ -970,7 +969,7 @@ subroutine acc_parallel_loop_firstprivate_with_reduction
 ! CHECK: %[[RED_R:.*]] = acc.reduction varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) name("r") -> !fir.ref<i32>
 ! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<i32>) reduction(%[[RED_R]] : !fir.ref<i32>)
 
-! Character is not a scalar type category.
+! Character gets loop firstprivate too.
 subroutine acc_parallel_loop_firstprivate_character
   integer :: i, n
   character(len=8) :: s
@@ -986,10 +985,10 @@ subroutine acc_parallel_loop_firstprivate_character
 ! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_character
 ! CHECK: %[[FP_S:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<!fir.char<1,8>>) recipe({{.*}}) name("s") -> !fir.ref<!fir.char<1,8>>
 ! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_S]] : !fir.ref<!fir.char<1,8>>)
-! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
-! CHECK: acc.loop combined(parallel)
+! CHECK: %[[FP_S_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<!fir.char<1,8>>) recipe({{.*}}) implicit(true) name("s") -> !fir.ref<!fir.char<1,8>>
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_S_LOOP]] : !fir.ref<!fir.char<1,8>>)
 
-! Derived type is composite, not scalar.
+! Derived type gets loop firstprivate too.
 subroutine acc_parallel_loop_firstprivate_derived
   type t
     integer :: x
@@ -1008,10 +1007,10 @@ subroutine acc_parallel_loop_firstprivate_derived
 ! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_derived
 ! CHECK: acc.firstprivate varPtr({{.*}}) recipe({{.*}}) name("p")
 ! CHECK: acc.parallel combined(loop) {{.*}}firstprivate
-! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
-! CHECK: acc.loop combined(parallel)
+! CHECK: acc.firstprivate varPtr({{.*}}) recipe({{.*}}) implicit(true) name("p")
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate
 
-! Pointer and allocatable scalars are boxed, so they stay compute-only.
+! Pointer and allocatable get loop firstprivate too.
 subroutine acc_parallel_loop_firstprivate_pointer
   integer, pointer :: p
   integer, target :: t
@@ -1029,8 +1028,8 @@ subroutine acc_parallel_loop_firstprivate_pointer
 ! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_pointer
 ! CHECK: %[[FP_P:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<!fir.box<!fir.ptr<i32>>>) recipe({{.*}}) name("p") -> !fir.ref<!fir.box<!fir.ptr<i32>>>
 ! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_P]] : !fir.ref<!fir.box<!fir.ptr<i32>>>)
-! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
-! CHECK: acc.loop combined(parallel)
+! CHECK: %[[FP_P_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<!fir.box<!fir.ptr<i32>>>) recipe({{.*}}) implicit(true) name("p") -> !fir.ref<!fir.box<!fir.ptr<i32>>>
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_P_LOOP]] : !fir.ref<!fir.box<!fir.ptr<i32>>>)
 
 subroutine acc_parallel_loop_firstprivate_allocatable
   integer, allocatable :: v
@@ -1048,8 +1047,8 @@ subroutine acc_parallel_loop_firstprivate_allocatable
 ! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_allocatable
 ! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<!fir.box<!fir.heap<i32>>>) recipe({{.*}}) name("v") -> !fir.ref<!fir.box<!fir.heap<i32>>>
 ! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<!fir.box<!fir.heap<i32>>>)
-! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
-! CHECK: acc.loop combined(parallel)
+! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<!fir.box<!fir.heap<i32>>>) recipe({{.*}}) implicit(true) name("v") -> !fir.ref<!fir.box<!fir.heap<i32>>>
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<!fir.box<!fir.heap<i32>>>)
 
 subroutine acc_parallel_loop_firstprivate_section
   integer :: i, n
@@ -1064,8 +1063,8 @@ subroutine acc_parallel_loop_firstprivate_section
 ! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_section
 ! CHECK: %[[FP_B:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<!fir.array<10xf32>>) bounds({{.*}}) recipe({{.*}}) name("b(1:5)") -> !fir.ref<!fir.array<10xf32>>
 ! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_B]] : !fir.ref<!fir.array<10xf32>>)
-! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
-! CHECK: acc.loop combined(parallel)
+! CHECK: %[[FP_B_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<!fir.array<10xf32>>) bounds({{.*}}) recipe({{.*}}) implicit(true) name("b(1:5)") -> !fir.ref<!fir.array<10xf32>>
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_B_LOOP]] : !fir.ref<!fir.array<10xf32>>)
 
 ! Non-combined parallel with a single loop is left to a later pass.
 subroutine acc_parallel_firstprivate_one_loop
diff --git a/flang/test/Lower/OpenACC/acc-serial-loop.f90 b/flang/test/Lower/OpenACC/acc-serial-loop.f90
index 2a2054f6fb805..52db45dbd2dfd 100644
--- a/flang/test/Lower/OpenACC/acc-serial-loop.f90
+++ b/flang/test/Lower/OpenACC/acc-serial-loop.f90
@@ -602,8 +602,7 @@ subroutine acc_serial_loop
 
 end subroutine acc_serial_loop
 
-! serial loop defaults to seq, so scalar firstprivate stays on the compute
-! construct only.
+! serial loop now gets loop firstprivate too (consistent with private/reduction).
 subroutine acc_serial_loop_firstprivate_scalar
   integer :: i, n, v
   real :: a(10)
@@ -618,10 +617,10 @@ subroutine acc_serial_loop_firstprivate_scalar
 ! CHECK-LABEL: func.func @_QPacc_serial_loop_firstprivate_scalar
 ! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
 ! CHECK: acc.serial combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
-! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
-! CHECK: acc.loop combined(serial)
+! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) implicit(true) name("v") -> !fir.ref<i32>
+! CHECK: acc.loop combined(serial) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<i32>)
 
-! serial loop is not combined parallel, even with independent.
+! serial loop with independent also gets loop firstprivate.
 subroutine acc_serial_loop_firstprivate_independent
   integer :: i, n, v
   real :: a(10)
@@ -636,6 +635,6 @@ subroutine acc_serial_loop_firstprivate_independent
 ! CHECK-LABEL: func.func @_QPacc_serial_loop_firstprivate_independent
 ! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
 ! CHECK: acc.serial combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
-! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
-! CHECK: acc.loop combined(serial)
+! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) implicit(true) name("v") -> !fir.ref<i32>
+! CHECK: acc.loop combined(serial) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<i32>)
 ! CHECK: } inclusiveUpperbound(array<i1: true>) independent

>From 4bf1d69aff7b4738c334f07f275b9ebb5e2a1580 Mon Sep 17 00:00:00 2001
From: Susan Tan <zujunt at nvidia.com>
Date: Mon, 14 Sep 2026 09:18:51 -0700
Subject: [PATCH 08/13] format

---
 flang/lib/Frontend/CompilerInvocation.cpp | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/flang/lib/Frontend/CompilerInvocation.cpp b/flang/lib/Frontend/CompilerInvocation.cpp
index 7189d9f06cd4a..36732cd7fadcc 100644
--- a/flang/lib/Frontend/CompilerInvocation.cpp
+++ b/flang/lib/Frontend/CompilerInvocation.cpp
@@ -1824,10 +1824,10 @@ bool CompilerInvocation::createFromArgs(
                                             llvm::StringRef{"whole"});
 
   // -f[no-]openacc-combined-loop-firstprivate
-  invoc.loweringOpts.setOpenACCCombinedLoopFirstprivate(args.hasFlag(
-      clang::options::OPT_fopenacc_combined_loop_firstprivate,
-      clang::options::OPT_fno_openacc_combined_loop_firstprivate,
-      /*default=*/true));
+  invoc.loweringOpts.setOpenACCCombinedLoopFirstprivate(
+      args.hasFlag(clang::options::OPT_fopenacc_combined_loop_firstprivate,
+                   clang::options::OPT_fno_openacc_combined_loop_firstprivate,
+                   /*default=*/true));
 
   if (auto *arg = args.getLastArg(clang::options::OPT_ffp_maxmin_behavior_EQ)) {
     auto value = Fortran::common::parseFPMaxminBehavior(arg->getValue());

>From b195d7f807b0b332fdf3fe76b1908ccd0641ac18 Mon Sep 17 00:00:00 2001
From: Susan Tan <zujunt at nvidia.com>
Date: Mon, 14 Sep 2026 09:22:03 -0700
Subject: [PATCH 09/13] add more tests

---
 .../test/Lower/OpenACC/acc-parallel-loop.f90  | 33 +++++++++++++++++
 flang/test/Lower/OpenACC/acc-serial-loop.f90  | 36 +++++++++++++++++++
 2 files changed, 69 insertions(+)

diff --git a/flang/test/Lower/OpenACC/acc-parallel-loop.f90 b/flang/test/Lower/OpenACC/acc-parallel-loop.f90
index 5ed965bf802d5..34c5bac67e554 100644
--- a/flang/test/Lower/OpenACC/acc-parallel-loop.f90
+++ b/flang/test/Lower/OpenACC/acc-parallel-loop.f90
@@ -728,6 +728,23 @@ subroutine acc_parallel_loop_firstprivate_auto
 ! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<i32>)
 ! CHECK: } inclusiveUpperbound(array<i1: true>) auto_
 
+! Not only scalars: array gets loop firstprivate too.
+subroutine acc_parallel_loop_firstprivate_array
+  integer :: i, n
+  real :: b(10)
+  n = 10
+  !$acc parallel loop firstprivate(b)
+  do i = 1, n
+    b(i) = 1.0
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_array
+! CHECK: %[[FP_B:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<!fir.array<10xf32>>) recipe({{.*}}) name("b") -> !fir.ref<!fir.array<10xf32>>
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_B]] : !fir.ref<!fir.array<10xf32>>)
+! CHECK: %[[FP_B_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<!fir.array<10xf32>>) recipe({{.*}}) implicit(true) name("b") -> !fir.ref<!fir.array<10xf32>>
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_B_LOOP]] : !fir.ref<!fir.array<10xf32>>)
+
 subroutine acc_parallel_loop_firstprivate_mixed
   integer :: i, n, v
   real :: b(10)
@@ -951,6 +968,22 @@ subroutine acc_parallel_loop_firstprivate_dummy(v)
 ! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) implicit(true) name("v") -> !fir.ref<i32>
 ! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<i32>)
 
+! Not only scalars: assumed-shape array dummy (boxed array).
+subroutine acc_parallel_loop_firstprivate_assumed_shape(b)
+  real :: b(:)
+  integer :: i
+  !$acc parallel loop firstprivate(b)
+  do i = 1, size(b)
+    b(i) = 1.0
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_assumed_shape
+! CHECK: acc.firstprivate varPtr({{.*}}) recipe({{.*}}) name("b")
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate
+! CHECK: acc.firstprivate varPtr({{.*}}) recipe({{.*}}) implicit(true) name("b")
+! CHECK: acc.loop combined(parallel) {{.*}}firstprivate
+
 subroutine acc_parallel_loop_firstprivate_with_reduction
   integer :: i, n, v, r
   n = 10
diff --git a/flang/test/Lower/OpenACC/acc-serial-loop.f90 b/flang/test/Lower/OpenACC/acc-serial-loop.f90
index 52db45dbd2dfd..fa6672762b6ee 100644
--- a/flang/test/Lower/OpenACC/acc-serial-loop.f90
+++ b/flang/test/Lower/OpenACC/acc-serial-loop.f90
@@ -638,3 +638,39 @@ subroutine acc_serial_loop_firstprivate_independent
 ! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) implicit(true) name("v") -> !fir.ref<i32>
 ! CHECK: acc.loop combined(serial) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<i32>)
 ! CHECK: } inclusiveUpperbound(array<i1: true>) independent
+
+! Not only scalars on serial loop: array gets loop firstprivate.
+subroutine acc_serial_loop_firstprivate_array
+  integer :: i, n
+  real :: b(10)
+  n = 10
+  !$acc serial loop firstprivate(b)
+  do i = 1, n
+    b(i) = 1.0
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_serial_loop_firstprivate_array
+! CHECK: %[[FP_B:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<!fir.array<10xf32>>) recipe({{.*}}) name("b") -> !fir.ref<!fir.array<10xf32>>
+! CHECK: acc.serial combined(loop) {{.*}}firstprivate(%[[FP_B]] : !fir.ref<!fir.array<10xf32>>)
+! CHECK: %[[FP_B_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<!fir.array<10xf32>>) recipe({{.*}}) implicit(true) name("b") -> !fir.ref<!fir.array<10xf32>>
+! CHECK: acc.loop combined(serial) {{.*}}firstprivate(%[[FP_B_LOOP]] : !fir.ref<!fir.array<10xf32>>)
+
+! Not only independent on serial loop: seq still gets loop firstprivate.
+subroutine acc_serial_loop_firstprivate_seq
+  integer :: i, n, v
+  real :: a(10)
+  n = 10
+  v = 7
+  !$acc serial loop seq firstprivate(v)
+  do i = 1, n
+    a(i) = v
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPacc_serial_loop_firstprivate_seq
+! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
+! CHECK: acc.serial combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
+! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) implicit(true) name("v") -> !fir.ref<i32>
+! CHECK: acc.loop combined(serial) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<i32>)
+! CHECK: } inclusiveUpperbound(array<i1: true>) seq

>From daccaa211a0c1d4d9819fb6d236e85eb75a3ff6c Mon Sep 17 00:00:00 2001
From: Susan Tan <zujunt at nvidia.com>
Date: Mon, 14 Sep 2026 09:24:36 -0700
Subject: [PATCH 10/13] format

---
 flang/test/Lower/OpenACC/acc-parallel-loop.f90 | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/flang/test/Lower/OpenACC/acc-parallel-loop.f90 b/flang/test/Lower/OpenACC/acc-parallel-loop.f90
index 34c5bac67e554..63ec22da62fd0 100644
--- a/flang/test/Lower/OpenACC/acc-parallel-loop.f90
+++ b/flang/test/Lower/OpenACC/acc-parallel-loop.f90
@@ -979,9 +979,9 @@ subroutine acc_parallel_loop_firstprivate_assumed_shape(b)
 end subroutine
 
 ! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_assumed_shape
-! CHECK: acc.firstprivate varPtr({{.*}}) recipe({{.*}}) name("b")
+! CHECK: acc.firstprivate var({{.*}}) recipe({{.*}}) name("b")
 ! CHECK: acc.parallel combined(loop) {{.*}}firstprivate
-! CHECK: acc.firstprivate varPtr({{.*}}) recipe({{.*}}) implicit(true) name("b")
+! CHECK: acc.firstprivate var({{.*}}) recipe({{.*}}) implicit(true) name("b")
 ! CHECK: acc.loop combined(parallel) {{.*}}firstprivate
 
 subroutine acc_parallel_loop_firstprivate_with_reduction

>From 79275943b2800e80ed104a1fc178a67c175e4e95 Mon Sep 17 00:00:00 2001
From: Susan Tan <zujunt at nvidia.com>
Date: Mon, 14 Sep 2026 09:31:19 -0700
Subject: [PATCH 11/13] tweak

---
 flang/include/flang/Lower/LoweringOptions.def  |  5 -----
 flang/include/flang/Support/Fortran-features.h |  5 +++--
 flang/lib/Frontend/CompilerInvocation.cpp      | 13 +++++++------
 flang/lib/Lower/OpenACC.cpp                    |  3 ++-
 4 files changed, 12 insertions(+), 14 deletions(-)

diff --git a/flang/include/flang/Lower/LoweringOptions.def b/flang/include/flang/Lower/LoweringOptions.def
index 2931a5edc56e9..61ccb2ac19bdd 100644
--- a/flang/include/flang/Lower/LoweringOptions.def
+++ b/flang/include/flang/Lower/LoweringOptions.def
@@ -97,10 +97,5 @@ ENUM_LOWERINGOPT(FPMaxminBehavior, Fortran::common::FPMaxminBehavior, 2, 0)
 /// 0 means no trapping. Bit values match IEEE_FLAG_TYPE encoding.
 ENUM_LOWERINGOPT(FPExceptionTraps, unsigned, 8, 0)
 
-/// If true (default), attach an implicit firstprivate on the loop of a
-/// combined construct (parallel loop, serial loop) in addition to the compute
-/// construct. Controlled by -f[no-]openacc-combined-loop-firstprivate.
-ENUM_LOWERINGOPT(OpenACCCombinedLoopFirstprivate, unsigned, 1, 1)
-
 #undef LOWERINGOPT
 #undef ENUM_LOWERINGOPT
diff --git a/flang/include/flang/Support/Fortran-features.h b/flang/include/flang/Support/Fortran-features.h
index 4b2ff0a227f61..6aea48b536bfd 100644
--- a/flang/include/flang/Support/Fortran-features.h
+++ b/flang/include/flang/Support/Fortran-features.h
@@ -61,8 +61,9 @@ ENUM_CLASS(LanguageFeature, BackslashEscapes, OldDebugLines,
     MultipleProgramUnitsOnSameLine, AllocatedForAssociated,
     OpenMPThreadprivateEquivalence, RelaxedCLocChecks, CudaPinned,
     OpenAccDefaultNoneScalarsStrict, OpenACCMultipleNamesInRoutine,
-    EnumerationType, CUDAInit, PreferIntrinsicModuleUseAssociation,
-    MultipleCommonBlockInit, OutOfBoundsSubscripts)
+    OpenACCCombinedLoopFirstprivate, EnumerationType, CUDAInit,
+    PreferIntrinsicModuleUseAssociation, MultipleCommonBlockInit,
+    OutOfBoundsSubscripts)
 
 // Portability and suspicious usage warnings
 ENUM_CLASS(UsageWarning, Portability, PointerToUndefinable,
diff --git a/flang/lib/Frontend/CompilerInvocation.cpp b/flang/lib/Frontend/CompilerInvocation.cpp
index 36732cd7fadcc..e4f42173107ae 100644
--- a/flang/lib/Frontend/CompilerInvocation.cpp
+++ b/flang/lib/Frontend/CompilerInvocation.cpp
@@ -950,6 +950,13 @@ static bool parseFrontendArgs(FrontendOptions &opts, llvm::opt::ArgList &args,
                    clang::options::OPT_fno_openacc_multiple_names_in_routine,
                    true));
 
+  // -f{no-}openacc-combined-loop-firstprivate
+  opts.features.Enable(
+      Fortran::common::LanguageFeature::OpenACCCombinedLoopFirstprivate,
+      args.hasFlag(clang::options::OPT_fopenacc_combined_loop_firstprivate,
+                   clang::options::OPT_fno_openacc_combined_loop_firstprivate,
+                   true));
+
   // -f{no-}prefer-intrinsic-module-use-association
   if (const auto *arg = args.getLastArg(
           clang::options::OPT_fprefer_intrinsic_module_use_association,
@@ -1823,12 +1830,6 @@ bool CompilerInvocation::createFromArgs(
     invoc.loweringOpts.setRepackArraysWhole(arg->getValue() ==
                                             llvm::StringRef{"whole"});
 
-  // -f[no-]openacc-combined-loop-firstprivate
-  invoc.loweringOpts.setOpenACCCombinedLoopFirstprivate(
-      args.hasFlag(clang::options::OPT_fopenacc_combined_loop_firstprivate,
-                   clang::options::OPT_fno_openacc_combined_loop_firstprivate,
-                   /*default=*/true));
-
   if (auto *arg = args.getLastArg(clang::options::OPT_ffp_maxmin_behavior_EQ)) {
     auto value = Fortran::common::parseFPMaxminBehavior(arg->getValue());
     invoc.getCodeGenOpts().setFPMaxminBehavior(value);
diff --git a/flang/lib/Lower/OpenACC.cpp b/flang/lib/Lower/OpenACC.cpp
index 5c23837d03471..d6ccaa312dbf5 100644
--- a/flang/lib/Lower/OpenACC.cpp
+++ b/flang/lib/Lower/OpenACC.cpp
@@ -1738,7 +1738,8 @@ static bool shouldAttachFirstprivateOnCombinedLoop(
   if (*combinedConstructs != mlir::acc::CombinedConstructsType::ParallelLoop &&
       *combinedConstructs != mlir::acc::CombinedConstructsType::SerialLoop)
     return false;
-  return converter.getLoweringOptions().getOpenACCCombinedLoopFirstprivate();
+  return converter.getFoldingContext().languageFeatures().IsEnabled(
+      Fortran::common::LanguageFeature::OpenACCCombinedLoopFirstprivate);
 }
 
 // Helper to visit Bounds of DO LOOP nest.

>From f0f22b92b3d6238f61645ba3604f5378b6ff09ac Mon Sep 17 00:00:00 2001
From: Susan Tan <zujunt at nvidia.com>
Date: Mon, 14 Sep 2026 10:13:39 -0700
Subject: [PATCH 12/13] tweak

---
 flang/include/flang/Lower/LoweringOptions.def  |  5 +++++
 flang/include/flang/Support/Fortran-features.h |  5 ++---
 flang/lib/Frontend/CompilerInvocation.cpp      | 13 ++++++-------
 flang/lib/Lower/OpenACC.cpp                    |  3 +--
 4 files changed, 14 insertions(+), 12 deletions(-)

diff --git a/flang/include/flang/Lower/LoweringOptions.def b/flang/include/flang/Lower/LoweringOptions.def
index 61ccb2ac19bdd..2931a5edc56e9 100644
--- a/flang/include/flang/Lower/LoweringOptions.def
+++ b/flang/include/flang/Lower/LoweringOptions.def
@@ -97,5 +97,10 @@ ENUM_LOWERINGOPT(FPMaxminBehavior, Fortran::common::FPMaxminBehavior, 2, 0)
 /// 0 means no trapping. Bit values match IEEE_FLAG_TYPE encoding.
 ENUM_LOWERINGOPT(FPExceptionTraps, unsigned, 8, 0)
 
+/// If true (default), attach an implicit firstprivate on the loop of a
+/// combined construct (parallel loop, serial loop) in addition to the compute
+/// construct. Controlled by -f[no-]openacc-combined-loop-firstprivate.
+ENUM_LOWERINGOPT(OpenACCCombinedLoopFirstprivate, unsigned, 1, 1)
+
 #undef LOWERINGOPT
 #undef ENUM_LOWERINGOPT
diff --git a/flang/include/flang/Support/Fortran-features.h b/flang/include/flang/Support/Fortran-features.h
index 6aea48b536bfd..4b2ff0a227f61 100644
--- a/flang/include/flang/Support/Fortran-features.h
+++ b/flang/include/flang/Support/Fortran-features.h
@@ -61,9 +61,8 @@ ENUM_CLASS(LanguageFeature, BackslashEscapes, OldDebugLines,
     MultipleProgramUnitsOnSameLine, AllocatedForAssociated,
     OpenMPThreadprivateEquivalence, RelaxedCLocChecks, CudaPinned,
     OpenAccDefaultNoneScalarsStrict, OpenACCMultipleNamesInRoutine,
-    OpenACCCombinedLoopFirstprivate, EnumerationType, CUDAInit,
-    PreferIntrinsicModuleUseAssociation, MultipleCommonBlockInit,
-    OutOfBoundsSubscripts)
+    EnumerationType, CUDAInit, PreferIntrinsicModuleUseAssociation,
+    MultipleCommonBlockInit, OutOfBoundsSubscripts)
 
 // Portability and suspicious usage warnings
 ENUM_CLASS(UsageWarning, Portability, PointerToUndefinable,
diff --git a/flang/lib/Frontend/CompilerInvocation.cpp b/flang/lib/Frontend/CompilerInvocation.cpp
index e4f42173107ae..36732cd7fadcc 100644
--- a/flang/lib/Frontend/CompilerInvocation.cpp
+++ b/flang/lib/Frontend/CompilerInvocation.cpp
@@ -950,13 +950,6 @@ static bool parseFrontendArgs(FrontendOptions &opts, llvm::opt::ArgList &args,
                    clang::options::OPT_fno_openacc_multiple_names_in_routine,
                    true));
 
-  // -f{no-}openacc-combined-loop-firstprivate
-  opts.features.Enable(
-      Fortran::common::LanguageFeature::OpenACCCombinedLoopFirstprivate,
-      args.hasFlag(clang::options::OPT_fopenacc_combined_loop_firstprivate,
-                   clang::options::OPT_fno_openacc_combined_loop_firstprivate,
-                   true));
-
   // -f{no-}prefer-intrinsic-module-use-association
   if (const auto *arg = args.getLastArg(
           clang::options::OPT_fprefer_intrinsic_module_use_association,
@@ -1830,6 +1823,12 @@ bool CompilerInvocation::createFromArgs(
     invoc.loweringOpts.setRepackArraysWhole(arg->getValue() ==
                                             llvm::StringRef{"whole"});
 
+  // -f[no-]openacc-combined-loop-firstprivate
+  invoc.loweringOpts.setOpenACCCombinedLoopFirstprivate(
+      args.hasFlag(clang::options::OPT_fopenacc_combined_loop_firstprivate,
+                   clang::options::OPT_fno_openacc_combined_loop_firstprivate,
+                   /*default=*/true));
+
   if (auto *arg = args.getLastArg(clang::options::OPT_ffp_maxmin_behavior_EQ)) {
     auto value = Fortran::common::parseFPMaxminBehavior(arg->getValue());
     invoc.getCodeGenOpts().setFPMaxminBehavior(value);
diff --git a/flang/lib/Lower/OpenACC.cpp b/flang/lib/Lower/OpenACC.cpp
index d6ccaa312dbf5..5c23837d03471 100644
--- a/flang/lib/Lower/OpenACC.cpp
+++ b/flang/lib/Lower/OpenACC.cpp
@@ -1738,8 +1738,7 @@ static bool shouldAttachFirstprivateOnCombinedLoop(
   if (*combinedConstructs != mlir::acc::CombinedConstructsType::ParallelLoop &&
       *combinedConstructs != mlir::acc::CombinedConstructsType::SerialLoop)
     return false;
-  return converter.getFoldingContext().languageFeatures().IsEnabled(
-      Fortran::common::LanguageFeature::OpenACCCombinedLoopFirstprivate);
+  return converter.getLoweringOptions().getOpenACCCombinedLoopFirstprivate();
 }
 
 // Helper to visit Bounds of DO LOOP nest.

>From a4bb994c3b51e3f1ee03cf9a14ec463afd43bada Mon Sep 17 00:00:00 2001
From: Susan Tan <zujunt at nvidia.com>
Date: Mon, 14 Sep 2026 13:49:18 -0700
Subject: [PATCH 13/13] add tests

---
 .../acc-combined-loop-firstprivate-flag.f90   | 55 +++++++++++++++++++
 .../test/Lower/OpenACC/acc-parallel-loop.f90  |  5 +-
 2 files changed, 58 insertions(+), 2 deletions(-)
 create mode 100644 flang/test/Lower/OpenACC/acc-combined-loop-firstprivate-flag.f90

diff --git a/flang/test/Lower/OpenACC/acc-combined-loop-firstprivate-flag.f90 b/flang/test/Lower/OpenACC/acc-combined-loop-firstprivate-flag.f90
new file mode 100644
index 0000000000000..ff5594d87a7ec
--- /dev/null
+++ b/flang/test/Lower/OpenACC/acc-combined-loop-firstprivate-flag.f90
@@ -0,0 +1,55 @@
+! Test that -fno-openacc-combined-loop-firstprivate keeps firstprivate on the
+! compute construct only, with no implicit loop firstprivate.
+
+! RUN: %flang_fc1 -fopenacc -fno-openacc-combined-loop-firstprivate -emit-hlfir %s -o - | FileCheck %s
+
+subroutine flag_off_scalar
+  integer :: i, n, v
+  real :: a(10)
+  n = 10
+  v = 7
+  !$acc parallel loop firstprivate(v)
+  do i = 1, n
+    a(i) = v
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPflag_off_scalar
+! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop combined(parallel)
+! CHECK: } inclusiveUpperbound(array<i1: true>) independent
+
+subroutine flag_off_array
+  integer :: i, n
+  real :: b(10)
+  n = 10
+  !$acc parallel loop firstprivate(b)
+  do i = 1, n
+    b(i) = 1.0
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPflag_off_array
+! CHECK: acc.firstprivate varPtr(%{{.*}} : !fir.ref<!fir.array<10xf32>>) recipe({{.*}}) name("b")
+! CHECK: acc.parallel combined(loop) {{.*}}firstprivate
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop combined(parallel)
+
+subroutine flag_off_serial_loop
+  integer :: i, n, v
+  real :: a(10)
+  n = 10
+  v = 7
+  !$acc serial loop firstprivate(v)
+  do i = 1, n
+    a(i) = v
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPflag_off_serial_loop
+! CHECK: acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v")
+! CHECK: acc.serial combined(loop) {{.*}}firstprivate
+! CHECK-NOT: acc.firstprivate {{.*}} implicit(true)
+! CHECK: acc.loop combined(serial)
diff --git a/flang/test/Lower/OpenACC/acc-parallel-loop.f90 b/flang/test/Lower/OpenACC/acc-parallel-loop.f90
index 63ec22da62fd0..1ed0c25d663ab 100644
--- a/flang/test/Lower/OpenACC/acc-parallel-loop.f90
+++ b/flang/test/Lower/OpenACC/acc-parallel-loop.f90
@@ -686,9 +686,10 @@ subroutine acc_parallel_loop_firstprivate_scalar
 end subroutine
 
 ! CHECK-LABEL: func.func @_QPacc_parallel_loop_firstprivate_scalar
-! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe({{.*}}) name("v") -> !fir.ref<i32>
+! CHECK: %[[FP_V:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe(@firstprivatization_ref_i32) name("v") -> !fir.ref<i32>
 ! CHECK: acc.parallel combined(loop) {{.*}}firstprivate(%[[FP_V]] : !fir.ref<i32>)
-! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr({{.*}} : !fir.ref<i32>) recipe({{.*}}) implicit(true) name("v") -> !fir.ref<i32>
+! CHECK: %[[DECL_V:.*]]:2 = hlfir.declare %[[FP_V]] {uniq_name = {{.*}}} : (!fir.ref<i32>) -> (!fir.ref<i32>, !fir.ref<i32>)
+! CHECK: %[[FP_V_LOOP:.*]] = acc.firstprivate varPtr(%[[DECL_V]]#0 : !fir.ref<i32>) recipe(@firstprivatization_ref_i32) implicit(true) name("v") -> !fir.ref<i32>
 ! CHECK: acc.loop combined(parallel) {{.*}}firstprivate(%[[FP_V_LOOP]] : !fir.ref<i32>)
 ! CHECK: } inclusiveUpperbound(array<i1: true>) independent
 



More information about the flang-commits mailing list