[flang-commits] [flang] [flang] Support declarations scoping in FIR AA. (PR #201216)

Slava Zakharin via flang-commits flang-commits at lists.llvm.org
Tue Jun 2 16:58:25 PDT 2026


https://github.com/vzakhari updated https://github.com/llvm/llvm-project/pull/201216

>From 9ee7a020b187b8a4cf56a9952c57274ec4b2c805 Mon Sep 17 00:00:00 2001
From: Slava Zakharin <szakharin at nvidia.com>
Date: Tue, 2 Jun 2026 14:48:28 -0700
Subject: [PATCH 1/2] [flang] Support declarations scoping in FIR AA.

Further experimentation with MLIR inlining showed that
FIR AA becomes more conservative once a subprogram is inlined.

For example:
```
subroutine caller(p1,p2)
  real, pointer :: p1,p1
  call callee(p1,p2)
end
subroutine callee(a1,a2)
  real :: a1,a2
  a1 = a2
end
```

After `callee` is inlined, FIR AA assumes that `a1` and `a2`
alias at the point of the assignment, because it classifies them
as pointer accesses.

This patch adds a machinery to collect declaration information
for multiple subprogram scopes (as currently defined by `fir.dummy_scope`),
so that FIR AA can use this information for better disambiguation.
---
 .../flang/Optimizer/Analysis/AliasAnalysis.h  |  99 +++++-
 .../lib/Optimizer/Analysis/AliasAnalysis.cpp  | 286 +++++++++++++++++-
 .../alias-analysis-scoped-origins.fir         | 172 +++++++++++
 .../Analysis/AliasAnalysis/ptr-component.fir  |  40 ++-
 4 files changed, 560 insertions(+), 37 deletions(-)
 create mode 100644 flang/test/Analysis/AliasAnalysis/alias-analysis-scoped-origins.fir

diff --git a/flang/include/flang/Optimizer/Analysis/AliasAnalysis.h b/flang/include/flang/Optimizer/Analysis/AliasAnalysis.h
index 115358613e383..12a9f33d46815 100644
--- a/flang/include/flang/Optimizer/Analysis/AliasAnalysis.h
+++ b/flang/include/flang/Optimizer/Analysis/AliasAnalysis.h
@@ -13,11 +13,13 @@
 #include "flang/Common/enum-set.h"
 #include "mlir/Analysis/AliasAnalysis.h"
 #include "mlir/IR/BuiltinAttributes.h"
+#include "mlir/IR/Dominance.h"
 #include "mlir/IR/SymbolTable.h"
 #include "mlir/IR/Value.h"
 #include "llvm/ADT/DenseMap.h"
 #include "llvm/ADT/PointerUnion.h"
 #include "llvm/ADT/SmallVector.h"
+#include <memory>
 
 namespace fir {
 
@@ -198,6 +200,58 @@ struct AliasAnalysis {
       void print(llvm::raw_ostream &os) const;
     };
 
+    /// A snapshot taken when getSource() walks through an [hl]fir.declare.
+    /// Records the Fortran procedure scope of the declare (the dummy_scope
+    /// SSA value -- nullptr for non-dummy frames), the declare's result SSA
+    /// value, and the access path and attributes accumulated FROM THE LEAF
+    /// UP TO (and including) this declare. alias() uses these snapshots to
+    /// rebuild intermediate Sources rooted at shared-scope declares via
+    /// buildSourceAtDeclare().
+    ///
+    /// Relationship to the enclosing Source's top-level fields:
+    ///   - Source::accessPath / Source::attributes / Source::approximateSource
+    ///     describe the walk's TERMINAL stop ("root-of-walk to leaf"). They
+    ///     are what every existing alias rule and external consumer reads.
+    ///   - The root-closest ScopedOrigin is a snapshot at the last
+    ///     declare crossed before that terminal stop. Because getSource()
+    ///     walks backwards (leaf -> root) and push_back()s each snapshot,
+    ///     this is the LAST element pushed, i.e. scopedOrigins.back() --
+    ///     see the ordering note on the scopedOrigins member below (front
+    ///     = leaf-closest, back = root-closest). Its accessPath is a
+    ///     prefix-truncation of Source::accessPath, and its attributes are
+    ///     a subset of Source::attributes; in the common case where the
+    ///     terminal stop IS a declare (dummy with dummy_scope, host-assoc,
+    ///     OpenMP private), they coincide.
+    /// We keep both because:
+    ///   1. alias(Source, Source, ...) and external consumers (AddAliasTags,
+    ///      print, downstream analyses) read the top-level fields.
+    ///   2. When the terminal stop is not a declare (e.g. fir.alloca,
+    ///      fir.address_of with TARGET-attributed global), the top-level
+    ///      captures attributes the root-closest snapshot does not.
+    ///   3. When the walk terminates at Unknown without crossing any
+    ///      declare, scopedOrigins is empty and the top-level is the only
+    ///      answer.
+    struct ScopedOrigin {
+      /// The dummy_scope SSA value governing the declare. May be null Value
+      /// when the declare has no explicit dummy_scope and no fir.dummy_scope
+      /// op dominates it (e.g. globals at module scope).
+      mlir::Value scope;
+      /// Result SSA value of the [hl]fir.declare op.
+      mlir::Value declValue;
+      /// Path from the declare (treated as root) to the leaf Value the
+      /// original getSource() call was started from, in root-to-leaf order.
+      AccessPath accessPath;
+      /// Attributes accumulated from the leaf up to and including this
+      /// declare (includes getAttrsFromVariable(declare) and any
+      /// path-acquired bits such as Pointer from intermediate box loads).
+      Attributes attributes;
+      /// Whether the path is approximate at the moment of the snapshot.
+      bool approximateSource{false};
+      /// Whether the walk was following data (vs. a box reference) at the
+      /// moment of the snapshot.
+      bool isData{false};
+    };
+
     SourceOrigin origin;
 
     /// Kind of the memory source.
@@ -213,6 +267,11 @@ struct AliasAnalysis {
     AccessPath accessPath;
     /// Source object is used in an internal procedure via host association.
     bool isCapturedInInternalProcedure{false};
+    /// Per-declare checkpoints collected as getSource() walked through
+    /// [hl]fir.declare operations, ordered from leaf-closest (front) to
+    /// root-closest (back). Empty when no declare was crossed (e.g. the
+    /// walk terminated at Unknown).
+    llvm::SmallVector<ScopedOrigin, 4> scopedOrigins;
 
     /// Print information about the memory source to `os`.
     void print(llvm::raw_ostream &os) const;
@@ -288,14 +347,34 @@ struct AliasAnalysis {
   /// If getLastInstantiationPoint is true, the search for the source
   /// will stop at [hl]fir.declare if it represents a dummy
   /// argument declaration (i.e. it has the dummy_scope operand).
+  /// If collectScopedOrigins is false, the per-declare ScopedOrigin
+  /// snapshots are not collected (used internally by buildSourceAtDeclare
+  /// to reuse getSource purely for declare classification without the
+  /// bookkeeping side effect).
   fir::AliasAnalysis::Source getSource(mlir::Value,
-                                       bool getLastInstantiationPoint = false);
+                                       bool getLastInstantiationPoint = false,
+                                       bool collectScopedOrigins = true);
 
   /// Return true, if `ty` is a reference type to a boxed
   /// POINTER object or a raw fir::PointerType.
   static bool isPointerReference(mlir::Type ty);
 
 private:
+  /// Build an intermediate Source rooted at the declare captured by the
+  /// snapshot. Reuses getSource(declValue) for the SourceKind / origin
+  /// classification (with collectScopedOrigins=false), then overrides
+  /// accessPath/attributes/approximateSource/origin.isData from the
+  /// snapshot so the returned Source represents "declare-as-root,
+  /// original-query-as-leaf".
+  Source buildSourceAtDeclare(const Source::ScopedOrigin &so);
+
+  /// Return the dummy_scope SSA value governing \p declareOp.
+  /// Prefers the declare's explicit getDummyScope() operand; otherwise
+  /// falls back to the result of the dominating fir.dummy_scope op in
+  /// the parent func. Returns a null Value when no scope is found
+  /// (e.g. globals at module scope).
+  mlir::Value getDeclarationScope(mlir::Operation *declareOp);
+
   /// Return true, if `ty` is a reference type to an object of derived type
   /// that contains a component with POINTER attribute.
   static bool isRecordWithPointerComponent(mlir::Type ty);
@@ -334,6 +413,24 @@ struct AliasAnalysis {
   /// a few values that are likely not globals.
   /// We can have both modes for different clients.
   llvm::DenseMap<mlir::Operation *, mlir::SymbolTable> symTabMap;
+
+  /// Per-function caches used by getDeclarationScope() to map a
+  /// fir.declare without an explicit dummy_scope operand to its
+  /// dominating fir.dummy_scope op. Lazily populated. Mirrors the
+  /// logic in flang/lib/Optimizer/Transforms/AddAliasTags.cpp::
+  /// PassState::processFunctionScopes / getDeclarationScope. The cache
+  /// stores fir.dummy_scope ops as mlir::Operation * pointers to avoid
+  /// pulling FIROps.h into this header; the .cpp casts back as needed.
+  ///
+  /// TODO: this duplicates the scope-mapping logic in AddAliasTags.cpp.
+  /// AddAliasTags should reuse AliasAnalysis::getDeclarationScope (and
+  /// the ScopedOrigin snapshots collected by getSource) instead of
+  /// maintaining its own PassState::processFunctionScopes, so the two
+  /// places cannot diverge.
+  llvm::DenseMap<mlir::Operation *, std::unique_ptr<mlir::DominanceInfo>>
+      domInfoCache;
+  llvm::DenseMap<mlir::Operation *, llvm::SmallVector<mlir::Operation *, 16>>
+      sortedScopeCache;
 };
 
 inline bool operator==(const AliasAnalysis::Source::SourceOrigin &lhs,
diff --git a/flang/lib/Optimizer/Analysis/AliasAnalysis.cpp b/flang/lib/Optimizer/Analysis/AliasAnalysis.cpp
index 5e92ed5f42665..42f4d5ec26206 100644
--- a/flang/lib/Optimizer/Analysis/AliasAnalysis.cpp
+++ b/flang/lib/Optimizer/Analysis/AliasAnalysis.cpp
@@ -16,6 +16,7 @@
 #include "flang/Optimizer/Support/InternalNames.h"
 #include "flang/Optimizer/Support/Utils.h"
 #include "mlir/Analysis/AliasAnalysis.h"
+#include "mlir/Dialect/Func/IR/FuncOps.h"
 #include "mlir/Dialect/OpenACC/OpenACC.h"
 #include "mlir/Dialect/OpenACC/OpenACCUtils.h"
 #include "mlir/Dialect/OpenMP/OpenMPDialect.h"
@@ -127,7 +128,8 @@ static fir::AliasAnalysis::Source getSourceForACCMappedValue(
             accumulatedAttrs,
             /*approximateSource=*/false,
             /*accessPath=*/{},
-            /*isCapturedInInternalProcedure=*/false};
+            /*isCapturedInInternalProcedure=*/false,
+            /*scopedOrigins=*/{}};
 
   // Not private-like: classify using the corresponding host variable's source.
   //
@@ -286,8 +288,74 @@ static fir::AliasAnalysis::Source mergeRegionBranchPredecessorSources(
 
   mlir::Type mergedTy = allTypesSame ? sources[0].valueType : fallbackType;
 
-  return {mergedOrigin, mergedKind, mergedTy,      mergedAttrs,
-          mergedApprox, mergedPath, mergedCaptured};
+  // Intersect scopedOrigins across predecessors by (scope, declValue).
+  // For an entry that matches in every predecessor, take the bitwise
+  // union (|=) of its 'attributes' and 'approximateSource' bits and
+  // keep the first predecessor's path steps (the steps must be equal
+  // to match, so there is nothing to merge there). Drop the entry on a
+  // path-step or isData mismatch, because different control-flow shapes
+  // from the same declare to the merge point cannot be summarised
+  // safely.
+  llvm::SmallVector<fir::AliasAnalysis::Source::ScopedOrigin, 4>
+      mergedScopedOrigins;
+  if (!sources.empty()) {
+    // Seed with the first predecessor's snapshots, keyed by
+    // (scope, declValue) for intersection lookups.
+    llvm::DenseMap<std::pair<void *, void *>, unsigned> indexInMerged;
+    mergedScopedOrigins.assign(sources[0].scopedOrigins.begin(),
+                               sources[0].scopedOrigins.end());
+    for (unsigned i = 0; i < mergedScopedOrigins.size(); ++i) {
+      const auto &scopedOrigin = mergedScopedOrigins[i];
+      indexInMerged[{scopedOrigin.scope.getAsOpaquePointer(),
+                     scopedOrigin.declValue.getAsOpaquePointer()}] = i;
+    }
+    llvm::SmallVector<bool, 4> seenThisPred;
+    for (unsigned predIdx = 1; predIdx < sources.size(); ++predIdx) {
+      seenThisPred.assign(mergedScopedOrigins.size(), false);
+      for (const auto &scopedOrigin : sources[predIdx].scopedOrigins) {
+        auto it =
+            indexInMerged.find({scopedOrigin.scope.getAsOpaquePointer(),
+                                scopedOrigin.declValue.getAsOpaquePointer()});
+        if (it == indexInMerged.end())
+          continue;
+        unsigned idx = it->second;
+        auto &mergedScopedOrigin = mergedScopedOrigins[idx];
+        if (mergedScopedOrigin.isData != scopedOrigin.isData ||
+            mergedScopedOrigin.accessPath.steps !=
+                scopedOrigin.accessPath.steps) {
+          // Mark as not-seen so the entry is dropped after this pred.
+          continue;
+        }
+        mergedScopedOrigin.attributes |= scopedOrigin.attributes;
+        mergedScopedOrigin.approximateSource |= scopedOrigin.approximateSource;
+        mergedScopedOrigin.accessPath.isApproximate |=
+            scopedOrigin.accessPath.isApproximate;
+        seenThisPred[idx] = true;
+      }
+      // Drop entries this predecessor did not match. Iterate in reverse
+      // to keep earlier indices valid while erasing.
+      for (int idx = static_cast<int>(mergedScopedOrigins.size()) - 1; idx >= 0;
+           --idx) {
+        if (!seenThisPred[idx]) {
+          const auto &scopedOrigin = mergedScopedOrigins[idx];
+          indexInMerged.erase({scopedOrigin.scope.getAsOpaquePointer(),
+                               scopedOrigin.declValue.getAsOpaquePointer()});
+          mergedScopedOrigins.erase(mergedScopedOrigins.begin() + idx);
+        }
+      }
+      // Recompute indices after erase shifts.
+      indexInMerged.clear();
+      for (unsigned i = 0; i < mergedScopedOrigins.size(); ++i) {
+        const auto &scopedOrigin = mergedScopedOrigins[i];
+        indexInMerged[{scopedOrigin.scope.getAsOpaquePointer(),
+                       scopedOrigin.declValue.getAsOpaquePointer()}] = i;
+      }
+    }
+  }
+
+  return {
+      mergedOrigin, mergedKind, mergedTy,       mergedAttrs,
+      mergedApprox, mergedPath, mergedCaptured, std::move(mergedScopedOrigins)};
 }
 
 namespace fir {
@@ -536,15 +604,49 @@ AliasResult AliasAnalysis::alias(mlir::Value lhs, mlir::Value rhs) {
   // through other dialects
   auto lhsSrc = getSource(lhs);
   auto rhsSrc = getSource(rhs);
-  return alias(lhsSrc, rhsSrc, lhs, rhs);
+  AliasResult result = alias(lhsSrc, rhsSrc, lhs, rhs);
+
+  // Scope-aware refinement after inlining: if both walks crossed declares
+  // in the SAME Fortran procedure scope at DISTINCT declare values, the
+  // two queries may still be disambiguated by rebuilding intermediate
+  // Sources rooted at each shared-scope declare pair (Fortran 2018
+  // 15.5.2.13: distinct dummy arguments / locals of the same procedure
+  // frame do not alias unless TARGET/POINTER/Cray attributes permit it).
+  // The per-pair check delegates back to the 4-arg alias() with paths and
+  // attributes snapshotted at the declares, so TARGET-attributed declares
+  // and pointer-dereferenced paths remain correctly reported as MayAlias.
+  // Short-circuit on NoAlias since any pair that disambiguates is
+  // decisive.
+  if (result == AliasResult::NoAlias || result == AliasResult::MustAlias)
+    return result;
+  for (const auto &lhsScopedOrigin : lhsSrc.scopedOrigins) {
+    if (!lhsScopedOrigin.scope)
+      continue;
+    for (const auto &rhsScopedOrigin : rhsSrc.scopedOrigins) {
+      if (lhsScopedOrigin.scope != rhsScopedOrigin.scope ||
+          lhsScopedOrigin.declValue == rhsScopedOrigin.declValue)
+        continue;
+      Source lhsInner = buildSourceAtDeclare(lhsScopedOrigin);
+      Source rhsInner = buildSourceAtDeclare(rhsScopedOrigin);
+      // Use the OUTER lhs/rhs values, not the declare values: the
+      // rebuilt Sources describe accessing the outer query's leaf
+      // through the captured declare. Passing the declares here would
+      // make type-based checks (e.g. descriptor-vs-data via
+      // noAliasBasedOnType) compare the declare's box-descriptor type
+      // to the outer leaf type and yield bogus NoAlias for pointer
+      // dereferences.
+      AliasResult refined = alias(lhsInner, rhsInner, lhs, rhs);
+      if (refined == AliasResult::NoAlias) {
+        LLVM_DEBUG(llvm::dbgs() << "  no alias via scoped-origin refinement\n");
+        return AliasResult::NoAlias;
+      }
+    }
+  }
+  return result;
 }
 
 AliasResult AliasAnalysis::alias(Source lhsSrc, Source rhsSrc, mlir::Value lhs,
                                  mlir::Value rhs) {
-  // TODO: alias() has to be aware of the function scopes.
-  // After MLIR inlining, the current implementation may
-  // not recognize non-aliasing entities.
-
   // If both values trace back to the same root through zero-offset view
   // operations (e.g. embox without slice, declare, convert), they access
   // the same underlying memory. This check avoids the case where
@@ -996,7 +1098,8 @@ static mlir::Value walkBlockArgPassThroughs(mlir::Value v) {
 }
 
 AliasAnalysis::Source AliasAnalysis::getSource(mlir::Value v,
-                                               bool getLastInstantiationPoint) {
+                                               bool getLastInstantiationPoint,
+                                               bool collectScopedOrigins) {
   // If v is a pass-through block argument (see walkBlockArgPassThroughs),
   // continue from the underlying operand so the tracking loop below has a
   // defining op to chew on. Without this, a recursive query like the one in
@@ -1025,6 +1128,13 @@ AliasAnalysis::Source AliasAnalysis::getSource(mlir::Value v,
   llvm::SmallVector<Source::PathStep, 4> pathSteps;
   Source::AccessPath accessPath;
   bool accessPathFinalized{false};
+
+  // Per-declare snapshots collected as the walk crosses [hl]fir.declare ops.
+  // Ordered from leaf-closest (front) to root-closest (back). Forwarded
+  // through region-branch merges and the box-load branch, then threaded into
+  // the final Source. Gated on collectScopedOrigins (suppressed when
+  // buildSourceAtDeclare reuses getSource purely for declare classification).
+  llvm::SmallVector<Source::ScopedOrigin, 4> scopedOrigins;
   while (defOp && !breakFromLoop) {
     // Operations may have multiple results, so we need to analyze
     // the result for which the source is queried.
@@ -1106,7 +1216,17 @@ AliasAnalysis::Source AliasAnalysis::getSource(mlir::Value v,
             if (isPointerBox)
               attributes.set(Attribute::Pointer);
 
-            auto boxSrc = getSource(op.getMemref());
+            // Keep the inner walk's getLastInstantiationPoint=false so it
+            // continues past dummy-scope declares to the underlying
+            // BlockArgument. The outer classification below relies on
+            // boxSrc.origin.u being the BlockArg (so isDummyArgument()
+            // succeeds and the outer SourceKind becomes Argument).
+            // Passing true here would stop the inner walk at the declare
+            // and force SourceKind::Indirect, which spuriously coarsens
+            // getCallModRef (e.g. for box_addr of allocatable dummies).
+            auto boxSrc = getSource(op.getMemref(),
+                                    /*getLastInstantiationPoint=*/false,
+                                    collectScopedOrigins);
             attributes |= boxSrc.attributes;
             approximateSource |= boxSrc.approximateSource;
             isCapturedInInternalProcedure |=
@@ -1133,6 +1253,30 @@ AliasAnalysis::Source AliasAnalysis::getSource(mlir::Value v,
                 boxSrc.accessPath.isApproximate || approximateSource;
             accessPathFinalized = true;
 
+            // Rebase each forwarded ScopedOrigin from the inner walk's
+            // coordinate system (rooted at the inner declare, leaf=memref)
+            // to the outer (leaf=original query) by splicing the deref
+            // step and the outer pathSteps onto the snapshot's path.
+            if (collectScopedOrigins) {
+              for (auto scopedOrigin : boxSrc.scopedOrigins) {
+                scopedOrigin.accessPath.steps.push_back(derefStep);
+                for (int i = pathSteps.size() - 1; i >= 0; --i)
+                  scopedOrigin.accessPath.steps.push_back(pathSteps[i]);
+                scopedOrigin.accessPath.isApproximate |= approximateSource;
+                if (isPointerBox)
+                  scopedOrigin.attributes.set(Attribute::Pointer);
+                scopedOrigin.approximateSource |= approximateSource;
+                // The inner walk computed isData against the box memref
+                // (typically false, since the walk started at
+                // !fir.ref<!fir.box<...>>). After splicing the deref step, the
+                // snapshot's path now describes reaching the outer query's leaf
+                // via the box's pointer, so it follows data iff the outer walk
+                // does.
+                scopedOrigin.isData = followingData;
+                scopedOrigins.push_back(std::move(scopedOrigin));
+              }
+            }
+
             global = llvm::dyn_cast<mlir::SymbolRefAttr>(boxSrc.origin.u);
             if (global) {
               type = SourceKind::Global;
@@ -1240,6 +1384,23 @@ AliasAnalysis::Source AliasAnalysis::getSource(mlir::Value v,
           attributes |= getAttrsFromVariable(varIf);
           isCapturedInInternalProcedure |=
               varIf.isCapturedInInternalProcedure();
+
+          // Snapshot a ScopedOrigin at this declare. The snapshot
+          // captures the path/attributes from the leaf to this declare
+          // and is used by alias() for scope-aware refinement.
+          if (collectScopedOrigins) {
+            Source::ScopedOrigin scopedOrigin;
+            scopedOrigin.scope = getDeclarationScope(op);
+            scopedOrigin.declValue = opResult;
+            scopedOrigin.accessPath.steps.assign(pathSteps.rbegin(),
+                                                 pathSteps.rend());
+            scopedOrigin.accessPath.isApproximate = approximateSource;
+            scopedOrigin.attributes = attributes;
+            scopedOrigin.approximateSource = approximateSource;
+            scopedOrigin.isData = followingData;
+            scopedOrigins.push_back(std::move(scopedOrigin));
+          }
+
           if (varIf.isHostAssoc()) {
             // Do not track past such DeclareOp, because it does not
             // currently provide any useful information. The host associated
@@ -1397,7 +1558,8 @@ AliasAnalysis::Source AliasAnalysis::getSource(mlir::Value v,
           accSourceReturn = getSourceForACCMappedValue(
               v, op.getOperation(),
               [&](mlir::Value x) {
-                return getSource(x, getLastInstantiationPoint);
+                return getSource(x, getLastInstantiationPoint,
+                                 collectScopedOrigins);
               },
               followingData, attributes);
           breakFromLoop = true;
@@ -1415,20 +1577,37 @@ AliasAnalysis::Source AliasAnalysis::getSource(mlir::Value v,
                                    attributes,
                                    /*approximateSource=*/true,
                                    /*accessPath=*/{},
-                                   isCapturedInInternalProcedure}};
+                                   isCapturedInInternalProcedure,
+                                   /*scopedOrigins=*/{}}};
             breakFromLoop = true;
             return;
           }
           llvm::SmallVector<AliasAnalysis::Source, 4> predSources;
           predSources.reserve(predecessors.size());
           for (mlir::Value pred : predecessors)
-            predSources.push_back(getSource(pred, getLastInstantiationPoint));
+            predSources.push_back(getSource(pred, getLastInstantiationPoint,
+                                            collectScopedOrigins));
           regionBranchReturn = mergeRegionBranchPredecessorSources(
               predSources, v, ty, followingData);
           regionBranchReturn->attributes |= attributes;
           regionBranchReturn->approximateSource |= approximateSource;
           regionBranchReturn->isCapturedInInternalProcedure |=
               isCapturedInInternalProcedure;
+          // Prepend the outer (leaf-closer) scopedOrigins -- declares
+          // already crossed between leaf and this region-branch op --
+          // to the merged predecessors' snapshots. The inner snapshots'
+          // paths are relative to the region-branch result (matching
+          // the existing approximation for the top-level accessPath
+          // composed across region-branch merges).
+          if (collectScopedOrigins && !scopedOrigins.empty()) {
+            llvm::SmallVector<Source::ScopedOrigin, 4> combined;
+            combined.reserve(scopedOrigins.size() +
+                             regionBranchReturn->scopedOrigins.size());
+            combined.append(scopedOrigins.begin(), scopedOrigins.end());
+            combined.append(regionBranchReturn->scopedOrigins.begin(),
+                            regionBranchReturn->scopedOrigins.end());
+            regionBranchReturn->scopedOrigins = std::move(combined);
+          }
           breakFromLoop = true;
         })
         .Default([&](auto op) {
@@ -1481,7 +1660,8 @@ AliasAnalysis::Source AliasAnalysis::getSource(mlir::Value v,
             attributes,
             approximateSource,
             accessPath,
-            isCapturedInInternalProcedure};
+            isCapturedInInternalProcedure,
+            std::move(scopedOrigins)};
   }
   return {{v, instantiationPoint, followingData},
           type,
@@ -1489,7 +1669,8 @@ AliasAnalysis::Source AliasAnalysis::getSource(mlir::Value v,
           attributes,
           approximateSource,
           accessPath,
-          isCapturedInInternalProcedure};
+          isCapturedInInternalProcedure,
+          std::move(scopedOrigins)};
 }
 
 const mlir::SymbolTable *
@@ -1504,4 +1685,79 @@ fir::AliasAnalysis::getNearestSymbolTable(mlir::Operation *from) {
   return &symTabMap.try_emplace(symTabOp, symTabOp).first->second;
 }
 
+mlir::Value
+fir::AliasAnalysis::getDeclarationScope(mlir::Operation *declareOp) {
+  assert(declareOp && "expected a non-null declare op");
+  // Prefer the declare's explicit dummy_scope operand when present.
+  if (auto hlfirDeclareOp = mlir::dyn_cast<hlfir::DeclareOp>(declareOp))
+    if (mlir::Value dummyScope = hlfirDeclareOp.getDummyScope())
+      return dummyScope;
+  if (auto firDeclareOp = mlir::dyn_cast<fir::DeclareOp>(declareOp))
+    if (mlir::Value dummyScope = firDeclareOp.getDummyScope())
+      return dummyScope;
+
+  // Otherwise look up the dominating fir.dummy_scope in the parent
+  // function. Mirrors PassState::getDeclarationScope in AddAliasTags.cpp.
+  auto func = declareOp->getParentOfType<mlir::func::FuncOp>();
+  if (!func)
+    return {};
+
+  mlir::Operation *funcOp = func.getOperation();
+  auto domIt = domInfoCache.find(funcOp);
+  if (domIt == domInfoCache.end()) {
+    auto inserted = domInfoCache.try_emplace(
+        funcOp, std::make_unique<mlir::DominanceInfo>(funcOp));
+    domIt = inserted.first;
+  }
+  mlir::DominanceInfo &domInfo = *domIt->second;
+
+  auto scopeIt = sortedScopeCache.find(funcOp);
+  if (scopeIt == sortedScopeCache.end()) {
+    llvm::SmallVector<mlir::Operation *, 16> scopeOps;
+    func.walk(
+        [&](fir::DummyScopeOp op) { scopeOps.push_back(op.getOperation()); });
+    llvm::stable_sort(scopeOps, [&](mlir::Operation *a, mlir::Operation *b) {
+      return domInfo.properlyDominates(a, b);
+    });
+    scopeIt = sortedScopeCache.insert({funcOp, std::move(scopeOps)}).first;
+  }
+
+  const auto &scopeOps = scopeIt->second;
+  for (auto it = scopeOps.rbegin(), ie = scopeOps.rend(); it != ie; ++it) {
+    if (domInfo.dominates(*it, declareOp))
+      return mlir::cast<fir::DummyScopeOp>(*it).getResult();
+  }
+  return {};
+}
+
+fir::AliasAnalysis::Source fir::AliasAnalysis::buildSourceAtDeclare(
+    const fir::AliasAnalysis::Source::ScopedOrigin &scopedOrigin) {
+  // Reuse getSource for classification (handles dummy_scope, alloca/
+  // allocmem, address_of, etc. exactly as the main walk does). Disable
+  // ScopedOrigin collection so we do not allocate snapshots that would
+  // be immediately discarded.
+  //
+  // Pass getLastInstantiationPoint=true so the walk STOPS at the captured
+  // declare and classifies the Source in that declare's own scope: a
+  // dummy-scope declare becomes SourceKind::Argument, while a local/global
+  // declare still continues to its alloca/address_of (Allocate/Global).
+  // This is essential after inlining: with getLastInstantiationPoint=false
+  // the walk would continue past the dummy declare through cross-scope
+  // chains (e.g. fir.embox/fir.box_addr/scf.if introduced by contiguity
+  // copy-in or OPTIONAL select in the caller frame), whose region-branch
+  // merge collapses the kind to SourceKind::Unknown and makes the 4-arg
+  // alias() report MayAlias -- defeating the whole point of the refinement.
+  Source source = getSource(scopedOrigin.declValue,
+                            /*getLastInstantiationPoint=*/true,
+                            /*collectScopedOrigins=*/false);
+  // Rebase path/attributes to the snapshot taken when the original
+  // walk crossed this declare, so the returned Source represents
+  // "declare-as-root, original-query-as-leaf".
+  source.accessPath = scopedOrigin.accessPath;
+  source.attributes = scopedOrigin.attributes;
+  source.approximateSource = scopedOrigin.approximateSource;
+  source.origin.isData = scopedOrigin.isData;
+  return source;
+}
+
 } // namespace fir
diff --git a/flang/test/Analysis/AliasAnalysis/alias-analysis-scoped-origins.fir b/flang/test/Analysis/AliasAnalysis/alias-analysis-scoped-origins.fir
new file mode 100644
index 0000000000000..afbb8046c4639
--- /dev/null
+++ b/flang/test/Analysis/AliasAnalysis/alias-analysis-scoped-origins.fir
@@ -0,0 +1,172 @@
+// Check the ScopedOrigin-based scope-aware refinement in FIR
+// AliasAnalysis. After FIR inlining, the AliasAnalysis::getSource
+// walk no longer terminates at the original [hl]fir.declare of a
+// dummy argument: an inlined call appears as a sequence of fir.box
+// / fir.declare / fir.dummy_scope / fir.embox operations that walk
+// past the variable's declaration. The scope-aware refinement uses
+// per-declare snapshots (ScopedOrigin) collected during the walk so
+// that pairs of declares dominated by the same fir.dummy_scope can
+// still be disambiguated.
+//
+// RUN: fir-opt %s -split-input-file -o /dev/null --mlir-disable-threading \
+// RUN:   -pass-pipeline='builtin.module(func.func(test-fir-alias-analysis))' \
+// RUN:   2>&1 | FileCheck -match-full-lines %s
+
+// -----
+
+// Two distinct, plain (non-TARGET, non-POINTER) dummy arguments of
+// the SAME procedure frame: Fortran 2018 15.5.2.13 says they cannot
+// alias. Without scope-aware refinement, the walks would terminate
+// at the Argument SourceKind but both Sources would have unrelated
+// roots and the alias() conservative path would report MayAlias.
+//
+// CHECK-LABEL: Testing : "_QPtest_two_dummies"
+// CHECK-DAG: x#0 <-> y#0: NoAlias
+func.func @_QPtest_two_dummies(
+    %arg0: !fir.ref<f32> {fir.bindc_name = "x"},
+    %arg1: !fir.ref<f32> {fir.bindc_name = "y"}) {
+  %scope = fir.dummy_scope : !fir.dscope
+  %x:2 = hlfir.declare %arg0 dummy_scope %scope
+      {test.ptr = "x", uniq_name = "_QFtest_two_dummiesEx"}
+      : (!fir.ref<f32>, !fir.dscope) -> (!fir.ref<f32>, !fir.ref<f32>)
+  %y:2 = hlfir.declare %arg1 dummy_scope %scope
+      {test.ptr = "y", uniq_name = "_QFtest_two_dummiesEy"}
+      : (!fir.ref<f32>, !fir.dscope) -> (!fir.ref<f32>, !fir.ref<f32>)
+  return
+}
+
+// -----
+
+// Distinct dummy and a local of the SAME procedure frame. The local
+// is also covered by the function's fir.dummy_scope (it is dominated
+// by the unique dummy_scope) and the local is not TARGET, so the
+// refinement returns NoAlias.
+//
+// CHECK-LABEL: Testing : "_QPtest_dummy_vs_local"
+// CHECK-DAG: x#0 <-> l#0: NoAlias
+func.func @_QPtest_dummy_vs_local(
+    %arg0: !fir.ref<f32> {fir.bindc_name = "x"}) {
+  %scope = fir.dummy_scope : !fir.dscope
+  %x:2 = hlfir.declare %arg0 dummy_scope %scope
+      {test.ptr = "x", uniq_name = "_QFtest_dummy_vs_localEx"}
+      : (!fir.ref<f32>, !fir.dscope) -> (!fir.ref<f32>, !fir.ref<f32>)
+  %alloca = fir.alloca f32 {bindc_name = "l", uniq_name = "_QFtest_dummy_vs_localEl"}
+  %l:2 = hlfir.declare %alloca
+      {test.ptr = "l", uniq_name = "_QFtest_dummy_vs_localEl"}
+      : (!fir.ref<f32>) -> (!fir.ref<f32>, !fir.ref<f32>)
+  return
+}
+
+// -----
+
+// One of the dummies has TARGET attribute. A pointer or another
+// TARGET could reach it, so the refinement must NOT downgrade the
+// result to NoAlias; it stays MayAlias.
+//
+// CHECK-LABEL: Testing : "_QPtest_target_dummy"
+// CHECK-DAG: xt#0 <-> y#0: MayAlias
+func.func @_QPtest_target_dummy(
+    %arg0: !fir.ref<f32> {fir.bindc_name = "xt"},
+    %arg1: !fir.ref<f32> {fir.bindc_name = "y"}) {
+  %scope = fir.dummy_scope : !fir.dscope
+  %xt:2 = hlfir.declare %arg0 dummy_scope %scope
+      {fortran_attrs = #fir.var_attrs<target>, test.ptr = "xt",
+       uniq_name = "_QFtest_target_dummyExt"}
+      : (!fir.ref<f32>, !fir.dscope) -> (!fir.ref<f32>, !fir.ref<f32>)
+  %y:2 = hlfir.declare %arg1 dummy_scope %scope
+      {fortran_attrs = #fir.var_attrs<target>, test.ptr = "y",
+       uniq_name = "_QFtest_target_dummyEy"}
+      : (!fir.ref<f32>, !fir.dscope) -> (!fir.ref<f32>, !fir.ref<f32>)
+  return
+}
+
+// -----
+
+// Same declare on both sides: the scope-aware refinement skips the
+// pair when the two snapshots share the same declValue, so the
+// original (correct) MustAlias / MayAlias result from the underlying
+// alias() is preserved.
+//
+// CHECK-LABEL: Testing : "_QPtest_same_declare"
+// CHECK-DAG: x#0 <-> x#1: MustAlias
+func.func @_QPtest_same_declare(
+    %arg0: !fir.ref<f32> {fir.bindc_name = "x"}) {
+  %scope = fir.dummy_scope : !fir.dscope
+  %x:2 = hlfir.declare %arg0 dummy_scope %scope
+      {test.ptr = "x", uniq_name = "_QFtest_same_declareEx"}
+      : (!fir.ref<f32>, !fir.dscope) -> (!fir.ref<f32>, !fir.ref<f32>)
+  return
+}
+
+// -----
+
+// Same as test_two_dummies, but expressed with fir.declare (single
+// result) instead of hlfir.declare. This mirrors production FIR after
+// convert-hlfir-to-fir, where the scope-aware refinement must still
+// disambiguate two plain dummies sharing one fir.dummy_scope.
+//
+// CHECK-LABEL: Testing : "_QPtest_two_dummies_fir"
+// CHECK-DAG: x.fir#0 <-> y.fir#0: NoAlias
+func.func @_QPtest_two_dummies_fir(
+    %arg0: !fir.ref<f32> {fir.bindc_name = "x"},
+    %arg1: !fir.ref<f32> {fir.bindc_name = "y"}) {
+  %scope = fir.dummy_scope : !fir.dscope
+  %x = fir.declare %arg0 dummy_scope %scope
+      {test.ptr = "x.fir", uniq_name = "_QFtest_two_dummies_firEx"}
+      : (!fir.ref<f32>, !fir.dscope) -> !fir.ref<f32>
+  %y = fir.declare %arg1 dummy_scope %scope
+      {test.ptr = "y.fir", uniq_name = "_QFtest_two_dummies_firEy"}
+      : (!fir.ref<f32>, !fir.dscope) -> !fir.ref<f32>
+  return
+}
+
+// -----
+
+// Two-level inlining: the queried values are distinct dummies of the
+// INNER (inlined) procedure frame, but each is only reachable through
+// a fir.if region-branch introduced in the caller frame (a contiguity
+// copy-in for `field`, an OPTIONAL presence select for `value`). The
+// else branches yield a different SourceKind (a local temp / fir.absent),
+// so the top-level getSource walk for each value merges to
+// SourceKind::Unknown and the underlying alias() reports MayAlias
+// ("indirect access"). The ONLY way to disambiguate is via the
+// ScopedOrigin snapshots taken at the inner-frame declares (which share
+// one fir.dummy_scope). This is a regression for buildSourceAtDeclare:
+// it must classify each rebuilt Source AT the captured declare's own
+// scope (getLastInstantiationPoint=true, yielding SourceKind::Argument)
+// rather than walking past the declare back into the fir.if and
+// collapsing to Unknown.
+//
+// CHECK-LABEL: Testing : "_QPtest_nested_inline_region_branch"
+// CHECK-DAG: field#0 <-> value#0: NoAlias
+func.func @_QPtest_nested_inline_region_branch(
+    %arg0: !fir.ref<f32> {fir.bindc_name = "field"},
+    %arg1: !fir.ref<f32> {fir.bindc_name = "value", fir.optional},
+    %cond: i1) {
+  // field: contiguity copy-in select -> original actual or a local temp.
+  %ftmp = fir.alloca f32
+  %f_sel = fir.if %cond -> (!fir.ref<f32>) {
+    fir.result %arg0 : !fir.ref<f32>
+  } else {
+    fir.result %ftmp : !fir.ref<f32>
+  }
+  // value: OPTIONAL presence select -> actual or absent.
+  %present = fir.is_present %arg1 : (!fir.ref<f32>) -> i1
+  %v_sel = fir.if %present -> (!fir.ref<f32>) {
+    fir.result %arg1 : !fir.ref<f32>
+  } else {
+    %absent = fir.absent !fir.ref<f32>
+    fir.result %absent : !fir.ref<f32>
+  }
+  // Inner (inlined) frame: distinct dummies sharing one fir.dummy_scope.
+  %inner = fir.dummy_scope : !fir.dscope
+  %f_inner = fir.declare %f_sel dummy_scope %inner
+      {fortran_attrs = #fir.var_attrs<intent_inout>, test.ptr = "field",
+       uniq_name = "_QFinnerEfield"}
+      : (!fir.ref<f32>, !fir.dscope) -> !fir.ref<f32>
+  %v_inner = fir.declare %v_sel dummy_scope %inner
+      {fortran_attrs = #fir.var_attrs<intent_in>, test.ptr = "value",
+       uniq_name = "_QFinnerEvalue"}
+      : (!fir.ref<f32>, !fir.dscope) -> !fir.ref<f32>
+  return
+}
diff --git a/flang/test/Analysis/AliasAnalysis/ptr-component.fir b/flang/test/Analysis/AliasAnalysis/ptr-component.fir
index 6fbe6796c4c25..1486e4b4aec88 100644
--- a/flang/test/Analysis/AliasAnalysis/ptr-component.fir
+++ b/flang/test/Analysis/AliasAnalysis/ptr-component.fir
@@ -469,15 +469,15 @@ func.func @_QMmPtest.fir(%arg0: !fir.ref<!fir.box<!fir.ptr<i32>>> {fir.bindc_nam
 // CHECK-DAG: argp.fir#0 <-> arg%p.fir#0: MayAlias
 // CHECK-DAG: argp.fir#0 <-> arg%i.fir#0: NoAlias
 //
-// TODO: Shouldn't these all be NoAlias?
-// However, argp.tgt is currently handled as Indirect, and we only
-// can disambiguate descriptor and data addresses.
-// CHECK-DAG: argp.tgt#0 <-> arg#0: MayAlias
+// Scope-aware refinement (ScopedOrigin) now disambiguates argp.tgt
+// against unrelated scope-mate variables: arg is a non-TARGET dummy
+// so a pointer component dereference cannot reach it.
+// CHECK-DAG: argp.tgt#0 <-> arg#0: NoAlias
 // CHECK-DAG: argp.tgt#0 <-> arg%p#0: NoAlias
-// CHECK-DAG: argp.tgt#0 <-> arg%i#0: MayAlias
-// CHECK-DAG: argp.tgt.fir#0 <-> arg.fir#0: MayAlias
+// CHECK-DAG: argp.tgt#0 <-> arg%i#0: NoAlias
+// CHECK-DAG: argp.tgt.fir#0 <-> arg.fir#0: NoAlias
 // CHECK-DAG: argp.tgt.fir#0 <-> arg%p.fir#0: NoAlias
-// CHECK-DAG: argp.tgt.fir#0 <-> arg%i.fir#0: MayAlias
+// CHECK-DAG: argp.tgt.fir#0 <-> arg%i.fir#0: NoAlias
 //
 // CHECK-DAG: arga#0 <-> arg#0: NoAlias
 // CHECK-DAG: arga#0 <-> arg%p#0: NoAlias
@@ -493,15 +493,14 @@ func.func @_QMmPtest.fir(%arg0: !fir.ref<!fir.box<!fir.ptr<i32>>> {fir.bindc_nam
 // CHECK-DAG: argp.fir#0 <-> glob%p.fir#0: MayAlias
 // CHECK-DAG: argp.fir#0 <-> glob%i.fir#0: NoAlias
 //
-// TODO: Shouldn't these all be NoAlias?
-// However, argp.tgt is currently handled as Indirect, and we only
-// can disambiguate descriptor and data addresses.
-// CHECK-DAG: argp.tgt#0 <-> glob#0: MayAlias
+// Scope-aware refinement (ScopedOrigin) now disambiguates argp.tgt
+// against the non-TARGET global glob.
+// CHECK-DAG: argp.tgt#0 <-> glob#0: NoAlias
 // CHECK-DAG: argp.tgt#0 <-> glob%p#0: NoAlias
-// CHECK-DAG: argp.tgt#0 <-> glob%i#0: MayAlias
-// CHECK-DAG: argp.tgt.fir#0 <-> glob.fir#0: MayAlias
+// CHECK-DAG: argp.tgt#0 <-> glob%i#0: NoAlias
+// CHECK-DAG: argp.tgt.fir#0 <-> glob.fir#0: NoAlias
 // CHECK-DAG: argp.tgt.fir#0 <-> glob%p.fir#0: NoAlias
-// CHECK-DAG: argp.tgt.fir#0 <-> glob%i.fir#0: MayAlias
+// CHECK-DAG: argp.tgt.fir#0 <-> glob%i.fir#0: NoAlias
 //
 // CHECK-DAG: arga#0 <-> glob#0: NoAlias
 // CHECK-DAG: arga#0 <-> glob%p#0: NoAlias
@@ -517,15 +516,14 @@ func.func @_QMmPtest.fir(%arg0: !fir.ref<!fir.box<!fir.ptr<i32>>> {fir.bindc_nam
 // CHECK-DAG: argp.fir#0 <-> loc%p.fir#0: NoAlias
 // CHECK-DAG: argp.fir#0 <-> loc%i.fir#0: NoAlias
 //
-// TODO: Shouldn't these all be NoAlias?
-// However, argp.tgt is currently handled as Indirect, and we only
-// can disambiguate descriptor and data addresses.
-// CHECK-DAG: argp.tgt#0 <-> loc#0: MayAlias
+// Scope-aware refinement (ScopedOrigin) now disambiguates argp.tgt
+// against the non-TARGET local loc.
+// CHECK-DAG: argp.tgt#0 <-> loc#0: NoAlias
 // CHECK-DAG: argp.tgt#0 <-> loc%p#0: NoAlias
-// CHECK-DAG: argp.tgt#0 <-> loc%i#0: MayAlias
-// CHECK-DAG: argp.tgt.fir#0 <-> loc.fir#0: MayAlias
+// CHECK-DAG: argp.tgt#0 <-> loc%i#0: NoAlias
+// CHECK-DAG: argp.tgt.fir#0 <-> loc.fir#0: NoAlias
 // CHECK-DAG: argp.tgt.fir#0 <-> loc%p.fir#0: NoAlias
-// CHECK-DAG: argp.tgt.fir#0 <-> loc%i.fir#0: MayAlias
+// CHECK-DAG: argp.tgt.fir#0 <-> loc%i.fir#0: NoAlias
 //
 // CHECK-DAG: arga#0 <-> loc#0: NoAlias
 // CHECK-DAG: arga#0 <-> loc%p#0: NoAlias

>From 6da8c744a3d88c889f2b79b5683b72f334155d1e Mon Sep 17 00:00:00 2001
From: Slava Zakharin <szakharin at nvidia.com>
Date: Tue, 2 Jun 2026 16:57:23 -0700
Subject: [PATCH 2/2] Added tests for chained declarations belonging to the
 same scope.

---
 .../alias-analysis-scoped-origins.fir         | 99 +++++++++++++++++++
 1 file changed, 99 insertions(+)

diff --git a/flang/test/Analysis/AliasAnalysis/alias-analysis-scoped-origins.fir b/flang/test/Analysis/AliasAnalysis/alias-analysis-scoped-origins.fir
index afbb8046c4639..6291ba86caccc 100644
--- a/flang/test/Analysis/AliasAnalysis/alias-analysis-scoped-origins.fir
+++ b/flang/test/Analysis/AliasAnalysis/alias-analysis-scoped-origins.fir
@@ -170,3 +170,102 @@ func.func @_QPtest_nested_inline_region_branch(
       : (!fir.ref<f32>, !fir.dscope) -> !fir.ref<f32>
   return
 }
+
+// -----
+
+// Chained declares in the SAME scope: an ASSOCIATE construct renames a
+// dummy argument (`associate(y => x)`). Lowering emits a plain
+// hlfir.declare for `y` (NO dummy_scope -- associate names are not
+// registered dummies) whose memref is the selector declare's result
+// %x#0. getDeclarationScope() assigns `y`'s snapshot the same dominating
+// fir.dummy_scope as `x`, so the refinement sees them as a same-scope,
+// distinct-declValue pair. They must NOT be disambiguated to NoAlias:
+// `y` is just another name for `x`. buildSourceAtDeclare walks THROUGH
+// the (dummy_scope-less) `y` declare down to the `x` declare, yielding
+// the same origin, and the base alias() already reports MustAlias via
+// the zero-offset view root, so the result stays MustAlias.
+//
+// CHECK-LABEL: Testing : "_QPtest_associate_rename_dummy"
+// CHECK-DAG: x#0 <-> y#0: MustAlias
+func.func @_QPtest_associate_rename_dummy(
+    %arg0: !fir.ref<f32> {fir.bindc_name = "x"}) {
+  %scope = fir.dummy_scope : !fir.dscope
+  %x:2 = hlfir.declare %arg0 dummy_scope %scope
+      {test.ptr = "x", uniq_name = "_QFtest_associate_rename_dummyEx"}
+      : (!fir.ref<f32>, !fir.dscope) -> (!fir.ref<f32>, !fir.ref<f32>)
+  // associate(y => x): chained declare, no dummy_scope.
+  %y:2 = hlfir.declare %x#0
+      {test.ptr = "y", uniq_name = "_QFtest_associate_rename_dummyEy"}
+      : (!fir.ref<f32>) -> (!fir.ref<f32>, !fir.ref<f32>)
+  return
+}
+
+// -----
+
+// Two associate-names each renaming a DISTINCT dummy argument
+// (`associate(a => x, b => y)`). The chained `a`/`b` declares carry no
+// dummy_scope; their memrefs trace through to the distinct dummy declares
+// %x / %y. Disambiguation must still succeed: a and b are distinct
+// entities and do not alias. (Here the base alias() already resolves the
+// chains to distinct Argument origins and returns NoAlias.)
+//
+// CHECK-LABEL: Testing : "_QPtest_associate_two_dummies"
+// CHECK-DAG: a#0 <-> b#0: NoAlias
+func.func @_QPtest_associate_two_dummies(
+    %arg0: !fir.ref<f32> {fir.bindc_name = "x"},
+    %arg1: !fir.ref<f32> {fir.bindc_name = "y"}) {
+  %scope = fir.dummy_scope : !fir.dscope
+  %x:2 = hlfir.declare %arg0 dummy_scope %scope
+      {uniq_name = "_QFtest_associate_two_dummiesEx"}
+      : (!fir.ref<f32>, !fir.dscope) -> (!fir.ref<f32>, !fir.ref<f32>)
+  %y:2 = hlfir.declare %arg1 dummy_scope %scope
+      {uniq_name = "_QFtest_associate_two_dummiesEy"}
+      : (!fir.ref<f32>, !fir.dscope) -> (!fir.ref<f32>, !fir.ref<f32>)
+  // associate(a => x, b => y): chained declares, no dummy_scope.
+  %a:2 = hlfir.declare %x#0
+      {test.ptr = "a", uniq_name = "_QFtest_associate_two_dummiesEa"}
+      : (!fir.ref<f32>) -> (!fir.ref<f32>, !fir.ref<f32>)
+  %b:2 = hlfir.declare %y#0
+      {test.ptr = "b", uniq_name = "_QFtest_associate_two_dummiesEb"}
+      : (!fir.ref<f32>) -> (!fir.ref<f32>, !fir.ref<f32>)
+  return
+}
+
+// -----
+
+// Two associate-names renaming the SAME array dummy
+// (`associate(a => v, b => v)`), with element accesses a(i) / b(j).
+// Both chained declares (no dummy_scope) trace through to the same dummy
+// declare %v, so the array-element accesses are approximate (statically
+// unknown indices) over the SAME origin. This exercises the scope-aware
+// refinement path (base result is MayAlias, not short-circuited): the
+// pair of chained `a`/`b` declares must rebuild to the same %v origin and
+// stay MayAlias, never falsely NoAlias.
+//
+// CHECK-LABEL: Testing : "_QPtest_associate_same_array"
+// CHECK-DAG: a_elem#0 <-> b_elem#0: MayAlias
+func.func @_QPtest_associate_same_array(
+    %arg0: !fir.ref<!fir.array<?xf32>> {fir.bindc_name = "v"},
+    %n: index, %i: index, %j: index) {
+  %scope = fir.dummy_scope : !fir.dscope
+  %shape = fir.shape %n : (index) -> !fir.shape<1>
+  %v = fir.declare %arg0(%shape) dummy_scope %scope
+      {fortran_attrs = #fir.var_attrs<intent_inout>,
+       uniq_name = "_QFtest_associate_same_arrayEv"}
+      : (!fir.ref<!fir.array<?xf32>>, !fir.shape<1>, !fir.dscope)
+        -> !fir.ref<!fir.array<?xf32>>
+  // associate(a => v, b => v): chained declares, no dummy_scope.
+  %a = fir.declare %v(%shape)
+      {uniq_name = "_QFtest_associate_same_arrayEa"}
+      : (!fir.ref<!fir.array<?xf32>>, !fir.shape<1>)
+        -> !fir.ref<!fir.array<?xf32>>
+  %b = fir.declare %v(%shape)
+      {uniq_name = "_QFtest_associate_same_arrayEb"}
+      : (!fir.ref<!fir.array<?xf32>>, !fir.shape<1>)
+        -> !fir.ref<!fir.array<?xf32>>
+  %a_elem = fir.array_coor %a(%shape) %i {test.ptr = "a_elem"}
+      : (!fir.ref<!fir.array<?xf32>>, !fir.shape<1>, index) -> !fir.ref<f32>
+  %b_elem = fir.array_coor %b(%shape) %j {test.ptr = "b_elem"}
+      : (!fir.ref<!fir.array<?xf32>>, !fir.shape<1>, index) -> !fir.ref<f32>
+  return
+}



More information about the flang-commits mailing list