[flang-commits] [flang] [flang][debug] Emit debug info for procedures from an INCLUDE'd file. (PR #225068)

Abid Qadeer via flang-commits flang-commits at lists.llvm.org
Mon Sep 21 04:44:08 PDT 2026


https://github.com/abidh created https://github.com/llvm/llvm-project/pull/225068

A declaration read through INCLUDE gets a location that is fused with the inclusion information, carrying a fir::LocationKindArrayAttr. debugInfoIsAlreadySet treated any fused location as one this pass had already handled, apart from a fir::LocationKindAttr that is never the attribute actually used, so handleFuncOp and handleGlobalOp returned immediately and such a procedure or global got no debug information at all.

Examine what the fusion holds instead of only whether there is one. The attribute to look for is now a template argument, so each caller names the one it would attach itself, a DISubprogramAttr for a function and an array of DIGlobalVariableExpressionAttr for a global, and no central list has to be kept in step with them.

Doing only that describes the procedure at the line of the INCLUDE statement rather than at its own, because the file and line are read with a cast that a fused location does not satisfy, leaving them at the compile unit's file and at line 1. Search the location for the FileLineColLoc in both places instead. Searching it also gives a variable inlined from another procedure the line it is declared on rather than that same fallback of 1, which is what changes in debug-dummy-argument-inline.fir.

>From 665d47f3e1c4161b3bc2223da1af779b485cc5c4 Mon Sep 17 00:00:00 2001
From: Abid Qadeer <haqadeer at amd.com>
Date: Mon, 21 Sep 2026 11:26:38 +0100
Subject: [PATCH] [flang][debug] Emit debug info for procedures from an
 INCLUDE'd file.

A declaration read through INCLUDE gets a location that is fused with the
inclusion information, carrying a fir::LocationKindArrayAttr.
debugInfoIsAlreadySet treated any fused location as one this pass had
already handled, apart from a fir::LocationKindAttr that is never the
attribute actually used, so handleFuncOp and handleGlobalOp returned
immediately and such a procedure or global got no debug information at all.

Examine what the fusion holds instead of only whether there is one. The
attribute to look for is now a template argument, so each caller names the
one it would attach itself, a DISubprogramAttr for a function and an array
of DIGlobalVariableExpressionAttr for a global, and no central list has to
be kept in step with them.

Doing only that describes the procedure at the line of the INCLUDE
statement rather than at its own, because the file and line are read with
a cast that a fused location does not satisfy, leaving them at the compile
unit's file and at line 1. Search the location for the FileLineColLoc in
both places instead. Searching it also gives a variable inlined from
another procedure the line it is declared on rather than that same
fallback of 1, which is what changes in debug-dummy-argument-inline.fir.

Co-authored-by: Cursor <cursoragent at cursor.com>
---
 flang/include/flang/Optimizer/Support/Utils.h |  6 ++-
 .../lib/Optimizer/Transforms/AddDebugInfo.cpp | 26 +++++++----
 flang/test/Integration/debug-include-file.f90 | 35 ++++++++++++++
 .../debug-dummy-argument-inline.fir           |  4 +-
 flang/test/Transforms/debug-include-file.fir  | 46 +++++++++++++++++++
 5 files changed, 104 insertions(+), 13 deletions(-)
 create mode 100644 flang/test/Integration/debug-include-file.f90
 create mode 100644 flang/test/Transforms/debug-include-file.fir

diff --git a/flang/include/flang/Optimizer/Support/Utils.h b/flang/include/flang/Optimizer/Support/Utils.h
index d2f0be15d7dd9..34f41b8cf9524 100644
--- a/flang/include/flang/Optimizer/Support/Utils.h
+++ b/flang/include/flang/Optimizer/Support/Utils.h
@@ -32,10 +32,12 @@
 #include "flang/Optimizer/CodeGen/TypeConverter.h"
 
 namespace fir {
-/// Return the line of a location, or 1 if it does not carry one.
+/// Return the line of a location, or 1 if it does not carry one. The location
+/// can be a fused one, e.g. for something read from an INCLUDE'd file, so
+/// search it rather than expecting a bare FileLineColLoc.
 inline uint32_t getLineFromLoc(mlir::Location loc) {
   uint32_t line = 1;
-  if (auto fileLoc = mlir::dyn_cast<mlir::FileLineColLoc>(loc))
+  if (auto fileLoc = loc->findInstanceOf<mlir::FileLineColLoc>())
     line = fileLoc.getLine();
   return line;
 }
diff --git a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
index 045caafa3e5f9..e710cf003bd4c 100644
--- a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
+++ b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
@@ -146,13 +146,17 @@ class AddDebugInfoPass : public fir::impl::AddDebugInfoBase<AddDebugInfoPass> {
                            fir::cg::XDeclareOp typeGenDeclOp);
 };
 
+/// Whether \p loc already carries debug information of type \c AttrT, fused
+/// onto it by this pass. A location is fused for unrelated reasons too, most
+/// notably one that came from an INCLUDE'd file, so what the fusion holds has
+/// to be examined rather than the fusion merely detected. Each caller names
+/// the attribute it would attach itself, so nothing here needs updating when
+/// another kind of debug information is generated elsewhere. Only the
+/// outermost fusion is examined, because that is the one this pass adds.
+template <typename AttrT>
 bool debugInfoIsAlreadySet(mlir::Location loc) {
-  if (mlir::isa<mlir::FusedLoc>(loc)) {
-    if (loc->findInstanceOf<mlir::FusedLocWith<fir::LocationKindAttr>>())
-      return false;
-    return true;
-  }
-  return false;
+  auto fusedLoc = mlir::dyn_cast<mlir::FusedLoc>(loc);
+  return fusedLoc && mlir::isa_and_present<AttrT>(fusedLoc.getMetadata());
 }
 
 // Generates the name for the artificial DISubprogram that we are going to
@@ -566,7 +570,8 @@ void AddDebugInfoPass::handleGlobalOp(fir::GlobalOp globalOp,
                                       fir::DebugTypeGenerator &typeGen,
                                       mlir::SymbolTable *symbolTable,
                                       fir::cg::XDeclareOp declOp) {
-  if (debugInfoIsAlreadySet(globalOp.getLoc()))
+  // A global is described by an array of DIGlobalVariableExpressionAttr.
+  if (debugInfoIsAlreadySet<mlir::ArrayAttr>(globalOp.getLoc()))
     return;
   mlir::MLIRContext *context = &getContext();
   mlir::OpBuilder builder(context);
@@ -644,8 +649,9 @@ void AddDebugInfoPass::handleFuncOp(mlir::func::FuncOp funcOp,
                                     mlir::SymbolTable *symbolTable) {
   mlir::Location l = funcOp->getLoc();
   // If fused location has already been created then nothing to do
-  // Otherwise, create a fused location.
-  if (debugInfoIsAlreadySet(l))
+  // Otherwise, create a fused location. A function is described by a
+  // DISubprogramAttr.
+  if (debugInfoIsAlreadySet<mlir::LLVM::DISubprogramAttr>(l))
     return;
 
   mlir::MLIRContext *context = &getContext();
@@ -656,7 +662,7 @@ void AddDebugInfoPass::handleFuncOp(mlir::func::FuncOp funcOp,
                         ? llvm::dwarf::getCallingConvention("DW_CC_program")
                         : llvm::dwarf::getCallingConvention("DW_CC_normal");
 
-  if (auto funcLoc = mlir::dyn_cast<mlir::FileLineColLoc>(l)) {
+  if (auto funcLoc = l->findInstanceOf<mlir::FileLineColLoc>()) {
     fileName = llvm::sys::path::filename(funcLoc.getFilename().getValue());
     filePath = llvm::sys::path::parent_path(funcLoc.getFilename().getValue());
   }
diff --git a/flang/test/Integration/debug-include-file.f90 b/flang/test/Integration/debug-include-file.f90
new file mode 100644
index 0000000000000..ab69921c98149
--- /dev/null
+++ b/flang/test/Integration/debug-include-file.f90
@@ -0,0 +1,35 @@
+! RUN: split-file %s %t
+! RUN: %flang_fc1 -emit-llvm -debug-info-kind=standalone -I %t %t/main.f90 -o - | FileCheck %s
+
+! A procedure written in an INCLUDE'd file gets a location that is fused with
+! the inclusion information. Check that it still gets debug information, and
+! that the information points at the file and line where the procedure is
+! written rather than at the INCLUDE statement.
+
+!--- body.f90
+! Nothing here starts on line 1 on purpose. 1 is also the line that is reported
+! when a position cannot be read out of the location, so checking for it would
+! pass whether or not the location was understood.
+subroutine included_sub(i)
+  integer :: i
+  i = 1
+  call inner()
+contains
+subroutine inner()
+  i = i + 1
+end subroutine
+end subroutine
+
+!--- main.f90
+include 'body.f90'
+program p
+  integer :: i
+  call included_sub(i)
+  print *, i
+end program
+
+! CHECK-DAG: ![[BODY:[0-9]+]] = !DIFile(filename: "body.f90"
+! CHECK-DAG: ![[SUB:[0-9]+]] = distinct !DISubprogram(name: "included_sub", linkageName: "included_sub_", {{.*}}file: ![[BODY]], line: 4, {{.*}}scopeLine: 4
+! CHECK-DAG: !DISubprogram(name: "inner", linkageName: "_QFincluded_subPinner", scope: ![[SUB]], file: ![[BODY]], line: 9, {{.*}}scopeLine: 9
+! CHECK-DAG: !DISubprogram(name: "p", linkageName: "_QQmain", {{.*}}file: ![[MAIN:[0-9]+]], line: 2
+! CHECK-DAG: ![[MAIN]] = !DIFile(filename: "main.f90"
diff --git a/flang/test/Transforms/debug-dummy-argument-inline.fir b/flang/test/Transforms/debug-dummy-argument-inline.fir
index 02f0d5c6539b4..e4ec5e182c078 100644
--- a/flang/test/Transforms/debug-dummy-argument-inline.fir
+++ b/flang/test/Transforms/debug-dummy-argument-inline.fir
@@ -2,8 +2,10 @@
 // procedure where the calls were inlined.
 // RUN: fir-opt --add-debug-info --mlir-print-debuginfo %s -o - | FileCheck %s
 
+// The inlined declaration is a callsite location, so its line is the one the
+// variable is declared on in the inlined procedure, not the line of the call.
 // CHECK: #di_local_variable = #llvm.di_local_variable<scope = #di_subprogram, name = "i", file = #di_file, line = 6, arg = 1, type = #di_basic_type>
-// CHECK: #di_local_variable1 = #llvm.di_local_variable<scope = #di_subprogram, name = "i", file = #di_file, line = 1, type = #di_basic_type>
+// CHECK: #di_local_variable1 = #llvm.di_local_variable<scope = #di_subprogram, name = "i", file = #di_file, line = 2, type = #di_basic_type>
 
 func.func @foo_(%arg0: !fir.ref<i32> {fir.bindc_name = "i"} loc("debug-dummy-argument-inline.f90":5:1)) attributes {fir.internal_name = "_QPfoo"} {
   %0 = fir.undefined !fir.dscope loc(#loc5)
diff --git a/flang/test/Transforms/debug-include-file.fir b/flang/test/Transforms/debug-include-file.fir
new file mode 100644
index 0000000000000..007c5932637a1
--- /dev/null
+++ b/flang/test/Transforms/debug-include-file.fir
@@ -0,0 +1,46 @@
+// RUN: fir-opt --add-debug-info --mlir-print-debuginfo %s | FileCheck %s
+
+// Lowering fuses the inclusion information onto the location of anything read
+// through an INCLUDE statement, which is the shape reproduced below. Check that
+// a procedure and a global carrying such a location still get debug
+// information, and that it names the file and line where they are written
+// rather than the INCLUDE statement.
+
+module attributes {dlti.dl_spec = #dlti.dl_spec<>} {
+  func.func @_QPincluded_sub(%arg0: !fir.ref<i32>) {
+    return loc(#ret_in_body)
+  } loc(#sub_in_body)
+
+  fir.global @_QMmEmodvar : i32 {
+    %c7_i32 = arith.constant 7 : i32 loc(#global_in_body)
+    fir.has_value %c7_i32 : i32 loc(#global_in_body)
+  } loc(#global_in_body)
+
+  func.func @_QQmain() {
+    return loc(#ret_in_main)
+  } loc(#prog_in_main)
+} loc(#module_loc)
+
+#module_loc = loc("main.f90":0:0)
+// The INCLUDE statement itself, on line 1 of the including file.
+#include_stmt = loc("main.f90":1:1)
+
+// Written in the included file, reached through the INCLUDE statement.
+#sub_pos = loc("body.f90":4:1)
+#sub_in_body = loc(fused<#fir<loc_kind_array[<base>, <inclusion>]>>[#sub_pos, #include_stmt])
+#ret_pos = loc("body.f90":6:1)
+#ret_in_body = loc(fused<#fir<loc_kind_array[<base>, <inclusion>]>>[#ret_pos, #include_stmt])
+#global_pos = loc("body.f90":2:14)
+#global_in_body = loc(fused<#fir<loc_kind_array[<base>, <inclusion>]>>[#global_pos, #include_stmt])
+
+// Written in the including file, for contrast.
+#prog_in_main = loc("main.f90":2:1)
+#ret_in_main = loc("main.f90":4:1)
+
+// CHECK-DAG: #[[MAIN:.*]] = #llvm.di_file<"main.f90" in "">
+// CHECK-DAG: #[[BODY:.*]] = #llvm.di_file<"body.f90" in "">
+// CHECK-DAG: #llvm.di_subprogram<{{.*}}name = "included_sub", linkageName = "_QPincluded_sub", file = #[[BODY]], line = 4, scopeLine = 4{{.*}}>
+// CHECK-DAG: #llvm.di_subprogram<{{.*}}name = "_QQmain", linkageName = "_QQmain", file = #[[MAIN]], line = 2, scopeLine = 2{{.*}}>
+// A global takes its file from the compile unit rather than from its own
+// location, so only the line is checked here.
+// CHECK-DAG: #llvm.di_global_variable<{{.*}}name = "modvar", linkageName = "_QMmEmodvar", {{.*}}line = 2{{.*}}>



More information about the flang-commits mailing list