Skip to content

[flang][debug] Add fake use ops for dynamic array dimension variables#200061

Open
timsmith78 wants to merge 2 commits into
llvm:mainfrom
timsmith78:user/timsmith78/preserve-arr-bounds
Open

[flang][debug] Add fake use ops for dynamic array dimension variables#200061
timsmith78 wants to merge 2 commits into
llvm:mainfrom
timsmith78:user/timsmith78/preserve-arr-bounds

Conversation

@timsmith78
Copy link
Copy Markdown
Contributor

In cases where the upper or lower bounds of a dynamic array are not explicitly referenced in code, flang can optimize away the internal variables that represent these values. This causes missing values to appear in the debugger when examining the dynamic array's type. Adding an llvm.fake.use op for each bound preserves it for use by a debugger, similar to the fix for #185432.

Resolves #119474

In cases where the upper or lower bounds of a dynamic array are not
explicitly referenced in code, flang can optimize away the internal
variables that represent these values.  This causes missing values
to appear in the debugger when examining the dynamic array's type.
Adding an llvm.fake.use op for each bound preserves it for use by
a debugger, similar to the fix for llvm#185432.

Resolves llvm#119474
@llvmorg-github-actions llvmorg-github-actions Bot added flang Flang issues not falling into any other category flang:fir-hlfir labels May 27, 2026
@llvmorg-github-actions
Copy link
Copy Markdown

@llvm/pr-subscribers-flang-fir-hlfir

Author: Tim (timsmith78)

Changes

In cases where the upper or lower bounds of a dynamic array are not explicitly referenced in code, flang can optimize away the internal variables that represent these values. This causes missing values to appear in the debugger when examining the dynamic array's type. Adding an llvm.fake.use op for each bound preserves it for use by a debugger, similar to the fix for #185432.

Resolves #119474


Full diff: https://github.com/llvm/llvm-project/pull/200061.diff

4 Files Affected:

  • (modified) flang/include/flang/Optimizer/Transforms/Passes.td (+2-2)
  • (modified) flang/lib/Optimizer/Passes/Pipelines.cpp (+1-1)
  • (modified) flang/lib/Optimizer/Transforms/AddDebugInfo.cpp (+55-1)
  • (modified) flang/test/Transforms/debug-fake-use.fir (+31-2)
diff --git a/flang/include/flang/Optimizer/Transforms/Passes.td b/flang/include/flang/Optimizer/Transforms/Passes.td
index d9072e7aab4f7..8c082fb073451 100644
--- a/flang/include/flang/Optimizer/Transforms/Passes.td
+++ b/flang/include/flang/Optimizer/Transforms/Passes.td
@@ -267,9 +267,9 @@ def AddDebugInfo : Pass<"add-debug-info", "mlir::ModuleOp"> {
     Option<"dwarfDebugFlags", "dwarf-debug-flags",
            "std::string", /*default=*/"std::string{}",
            "Command-line flags to append to DWARF producer">,
-    Option<"emitFakeUseForArguments", "emit-fake-use-for-arguments",
+    Option<"emitFakeUseForDebugVars", "emit-fake-use-for-debug-vars",
            "bool", /*default=*/"false",
-           "Emit fake use for function arguments to extend their lifetime">
+           "Emit fake use for debug variables to extend their lifetime">
   ];
 }
 
diff --git a/flang/lib/Optimizer/Passes/Pipelines.cpp b/flang/lib/Optimizer/Passes/Pipelines.cpp
index 2b80da308a7d4..8c3528b588a18 100644
--- a/flang/lib/Optimizer/Passes/Pipelines.cpp
+++ b/flang/lib/Optimizer/Passes/Pipelines.cpp
@@ -113,7 +113,7 @@ void addDebugInfoPass(mlir::PassManager &pm,
   options.dwarfVersion = config.DwarfVersion;
   options.splitDwarfFile = config.SplitDwarfFile;
   options.dwarfDebugFlags = config.DwarfDebugFlags;
-  options.emitFakeUseForArguments =
+  options.emitFakeUseForDebugVars =
       (config.OptLevel == llvm::OptimizationLevel::O0) &&
       !disableArgumentFakeUse;
   addPassConditionally(pm, disableDebugInfo,
diff --git a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
index e0b570a908521..5aaf66baf9c9d 100644
--- a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
+++ b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
@@ -307,7 +307,7 @@ void AddDebugInfoPass::handleLocalVariable(Op declOp, llvm::StringRef name,
   if (dummyScope && declOp.getDummyScope() == dummyScope) {
     if (auto argNoOpt = declOp.getDummyArgNo()) {
       argNo = *argNoOpt;
-      if (emitFakeUseForArguments) {
+      if (emitFakeUseForDebugVars) {
         if constexpr (std::is_same_v<Op, fir::cg::XDeclareOp>) {
           if (auto funcOp =
                   declOp->template getParentOfType<mlir::func::FuncOp>()) {
@@ -331,6 +331,60 @@ void AddDebugInfoPass::handleLocalVariable(Op declOp, llvm::StringRef name,
   auto tyAttr =
       typeGen.convertType(typeToConvert, fileAttr, scopeAttr, typeGenDeclOp);
 
+  // Check if tyAttr represents a dynamic Fortran array. If so, create
+  // FakeUseOps to preserve the automatic variables that represent the lower
+  // bound, count, and stride.
+  if (emitFakeUseForDebugVars) {
+    if (auto arrayTy =
+            mlir::dyn_cast<mlir::LLVM::DICompositeTypeAttr>(tyAttr)) {
+      if (arrayTy.getTag() == llvm::dwarf::DW_TAG_array_type) {
+        bool isDynamic = llvm::any_of(
+            arrayTy.getElements(), [](mlir::Attribute element) {
+              auto subrange =
+                  mlir::dyn_cast<mlir::LLVM::DISubrangeAttr>(element);
+              if (!subrange)
+                return false;
+              return static_cast<bool>(
+                  mlir::dyn_cast_if_present<
+                      mlir::LLVM::DILocalVariableAttr>(subrange.getCount()) ||
+                  mlir::dyn_cast_if_present<
+                      mlir::LLVM::DILocalVariableAttr>(
+                      subrange.getLowerBound()) ||
+                  mlir::dyn_cast_if_present<
+                      mlir::LLVM::DILocalVariableAttr>(
+                      subrange.getStride()));
+            });
+        if (isDynamic) {
+          if constexpr (std::is_same_v<Op, fir::cg::XDeclareOp>) {
+            if (auto funcOp =
+                    declOp->template getParentOfType<mlir::func::FuncOp>()) {
+              if (declOp->getBlock() == &funcOp.getBody().front()) {
+                for (mlir::Block &block : funcOp.getBody()) {
+                  if (auto returnOp = mlir::dyn_cast<mlir::func::ReturnOp>(
+                          block.getTerminator())) {
+                    mlir::OpBuilder::InsertionGuard guard(builder);
+                    builder.setInsertionPoint(returnOp);
+                    // Preserve count if it is a variable and not a constant.
+                    // Count is represented by shape in the declOp.
+                    for (auto val : declOp.getShape())
+                      if (!fir::getIntIfConstant(val))
+                        fir::FakeUseOp::create(builder, declOp.getLoc(), val);
+                    // Preserve lower bound if it is a variable and not a
+                    // constant.
+                    // Lower bound is represented by shift in the declOp.
+                    for (auto val : declOp.getShift())
+                      if (!fir::getIntIfConstant(val))
+                        fir::FakeUseOp::create(builder, declOp.getLoc(), val);
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
   auto localVarAttr = mlir::LLVM::DILocalVariableAttr::get(
       context, scopeAttr, mlir::StringAttr::get(context, name), fileAttr,
       getLineFromLoc(declOp.getLoc()), argNo, /* alignInBits*/ 0, tyAttr,
diff --git a/flang/test/Transforms/debug-fake-use.fir b/flang/test/Transforms/debug-fake-use.fir
index d86cc9b3afee4..e0aea80adf845 100644
--- a/flang/test/Transforms/debug-fake-use.fir
+++ b/flang/test/Transforms/debug-fake-use.fir
@@ -1,5 +1,5 @@
-// RUN: fir-opt --add-debug-info="emit-fake-use-for-arguments=true" %s | FileCheck %s --check-prefix=FAKE-USE
-// RUN: fir-opt --add-debug-info="emit-fake-use-for-arguments=false" %s | FileCheck %s --check-prefix=NO-FAKE-USE
+// RUN: fir-opt --add-debug-info="emit-fake-use-for-debug-vars=true" %s | FileCheck %s --check-prefix=FAKE-USE
+// RUN: fir-opt --add-debug-info="emit-fake-use-for-debug-vars=false" %s | FileCheck %s --check-prefix=NO-FAKE-USE
 
 // FAKE-USE-LABEL: func.func @test_
 // FAKE-USE: %[[UNDEF:.*]] = fir.undefined !fir.dscope
@@ -31,6 +31,19 @@
 // NO-FAKE-USE-NOT: fir.fake_use
 // NO-FAKE-USE: return
 
+// FAKE-USE-LABEL: func.func @test_dynamic_array
+// FAKE-USE: fircg.ext_declare %arg0(%[[COUNT:.*]]) origin %[[LB:.*]] dummy_scope
+// FAKE-USE: fir.call @foo() : () -> ()
+// FAKE-USE: fir.fake_use %[[COUNT]]
+// FAKE-USE: fir.fake_use %[[LB]]
+// FAKE-USE: return
+
+// NO-FAKE-USE-LABEL: func.func @test_dynamic_array
+// NO-FAKE-USE: fircg.ext_declare %arg0(%{{.*}}) origin %{{.*}} dummy_scope
+// NO-FAKE-USE: fir.call @foo() : () -> ()
+// NO-FAKE-USE-NOT: fir.fake_use
+// NO-FAKE-USE: return
+
 #loc1 = loc("debug-fake-use.f90":1:1)
 #loc3 = loc("debug-fake-use.f90":3:14)
 #loc4 = loc("debug-fake-use.f90":4:14)
@@ -53,4 +66,20 @@ module attributes {dlti.dl_spec = #dlti.dl_spec<i128 = dense<128> : vector<2xi64
     fir.call @foo() : () -> ()
     return loc(#loc5)
   } loc(#loc1)
+
+  func.func @test_dynamic_array(%arg0: !fir.ref<!fir.array<?xi32>> {fir.bindc_name = "arr"} loc("debug-fake-use.f90":1:1), %arg1: !fir.ref<i32> {fir.bindc_name = "n"} loc("debug-fake-use.f90":1:1), %arg2: !fir.ref<i32> {fir.bindc_name = "lb"} loc("debug-fake-use.f90":1:1)) attributes {fir.internal_name = "_QPtest_dynamic_array"} {
+    %c0 = arith.constant 0 : index
+    %0 = fir.undefined !fir.dscope loc(#loc1)
+    %1 = fircg.ext_declare %arg1 dummy_scope %0 arg 2 {uniq_name = "_QFtest_dynamic_arrayEn"} : (!fir.ref<i32>, !fir.dscope) -> !fir.ref<i32> loc(#loc3)
+    %2 = fircg.ext_declare %arg2 dummy_scope %0 arg 3 {uniq_name = "_QFtest_dynamic_arrayElb"} : (!fir.ref<i32>, !fir.dscope) -> !fir.ref<i32> loc(#loc4)
+    %3 = fir.load %1 : !fir.ref<i32>
+    %4 = fir.convert %3 : (i32) -> index
+    %5 = arith.cmpi sgt, %4, %c0 : index
+    %6 = arith.select %5, %4, %c0 : index
+    %7 = fir.load %2 : !fir.ref<i32>
+    %8 = fir.convert %7 : (i32) -> index
+    %9 = fircg.ext_declare %arg0(%6) origin %8 dummy_scope %0 arg 1 {uniq_name = "_QFtest_dynamic_arrayEarr"} : (!fir.ref<!fir.array<?xi32>>, index, index, !fir.dscope) -> !fir.ref<!fir.array<?xi32>> loc(#loc3)
+    fir.call @foo() : () -> ()
+    return loc(#loc5)
+  } loc(#loc1)
 } loc(#loc)

@github-actions
Copy link
Copy Markdown

⚠️ C/C++ code formatter, clang-format found issues in your code. ⚠️

You can test this locally with the following command:
git-clang-format --diff origin/main HEAD --extensions cpp -- flang/lib/Optimizer/Passes/Pipelines.cpp flang/lib/Optimizer/Transforms/AddDebugInfo.cpp --diff_from_common_commit

⚠️
The reproduction instructions above might return results for more than one PR
in a stack if you are using a stacked PR workflow. You can limit the results by
changing origin/main to the base branch/commit you want to compare against.
⚠️

View the diff from clang-format here.
diff --git a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
index 5aaf66baf..338235b29 100644
--- a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
+++ b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
@@ -338,20 +338,18 @@ void AddDebugInfoPass::handleLocalVariable(Op declOp, llvm::StringRef name,
     if (auto arrayTy =
             mlir::dyn_cast<mlir::LLVM::DICompositeTypeAttr>(tyAttr)) {
       if (arrayTy.getTag() == llvm::dwarf::DW_TAG_array_type) {
-        bool isDynamic = llvm::any_of(
-            arrayTy.getElements(), [](mlir::Attribute element) {
+        bool isDynamic =
+            llvm::any_of(arrayTy.getElements(), [](mlir::Attribute element) {
               auto subrange =
                   mlir::dyn_cast<mlir::LLVM::DISubrangeAttr>(element);
               if (!subrange)
                 return false;
               return static_cast<bool>(
-                  mlir::dyn_cast_if_present<
-                      mlir::LLVM::DILocalVariableAttr>(subrange.getCount()) ||
-                  mlir::dyn_cast_if_present<
-                      mlir::LLVM::DILocalVariableAttr>(
+                  mlir::dyn_cast_if_present<mlir::LLVM::DILocalVariableAttr>(
+                      subrange.getCount()) ||
+                  mlir::dyn_cast_if_present<mlir::LLVM::DILocalVariableAttr>(
                       subrange.getLowerBound()) ||
-                  mlir::dyn_cast_if_present<
-                      mlir::LLVM::DILocalVariableAttr>(
+                  mlir::dyn_cast_if_present<mlir::LLVM::DILocalVariableAttr>(
                       subrange.getStride()));
             });
         if (isDynamic) {

@jeanPerier jeanPerier requested review from abidh and jeanPerier May 28, 2026 09:28
@timsmith78
Copy link
Copy Markdown
Contributor Author

I accidentally clicked the "Update branch" button thinking it would rebase this branch on top of main, but it performed a merge instead. Should I delete this branch and push a new one that I've rebased? I don't want to mess up the commit history. I'd fix the issue clang-format cited at the same time...

declOp->template getParentOfType<mlir::func::FuncOp>()) {
if (declOp->getBlock() == &funcOp.getBody().front()) {
for (mlir::Block &block : funcOp.getBody()) {
if (auto returnOp = mlir::dyn_cast<mlir::func::ReturnOp>(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code to insert fake use ops here largely duplicates the existing code for dummy arguments. I wonder if we can move that code to a helper and then use that in both places. The getIntIfConstant check added here would also be safe to apply in the argument case.

if (auto arrayTy =
mlir::dyn_cast<mlir::LLVM::DICompositeTypeAttr>(tyAttr)) {
if (arrayTy.getTag() == llvm::dwarf::DW_TAG_array_type) {
bool isDynamic = llvm::any_of(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bool isDynamic = llvm::any_of(
bool isDynamic =
llvm::any_of(declOp.getShape(),
[](mlir::Value v) { return !fir::getIntIfConstant(v); }) ||
llvm::any_of(declOp.getShift(),
[](mlir::Value v) { return !fir::getIntIfConstant(v); });

Do you think something like this will work better? It asks declOp directly if it has non constant shape or size and does not depend on the translated type. Also I don't think we generate an artificial variable for stride so the stride check in your patch was probably redundant.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is actually the point of isDynamic? It does not look like it is saving much processing time compared to directly going through the declOp below even if there is nothing to do, and it is adding compilation time when there are fake use to emit.

I would suggest removing it.

%9 = fircg.ext_declare %arg0(%6) origin %8 dummy_scope %0 arg 1 {uniq_name = "_QFtest_dynamic_arrayEarr"} : (!fir.ref<!fir.array<?xi32>>, index, index, !fir.dscope) -> !fir.ref<!fir.array<?xi32>> loc(#loc3)
fir.call @foo() : () -> ()
return loc(#loc5)
} loc(#loc1)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can increase the test coverage a bit.

  1. Not specific to your case but I think having a function with multiple returns would be a useful test.
  2. Multi-dimensional array and/or having variable size with constant lower bound.

Copy link
Copy Markdown
Contributor

@jeanPerier jeanPerier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, makes sense to me to emit this.
Should this also be emitted for character length when relevant?

if (auto arrayTy =
mlir::dyn_cast<mlir::LLVM::DICompositeTypeAttr>(tyAttr)) {
if (arrayTy.getTag() == llvm::dwarf::DW_TAG_array_type) {
bool isDynamic = llvm::any_of(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is actually the point of isDynamic? It does not look like it is saving much processing time compared to directly going through the declOp below even if there is nothing to do, and it is adding compilation time when there are fake use to emit.

I would suggest removing it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

flang:fir-hlfir flang Flang issues not falling into any other category

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[flang][debug] Missing upper bound for adjustable array

3 participants