[flang-commits] [flang] [flang][debug] Add accurate line number information to module. (PR #214011)

Abid Qadeer via flang-commits flang-commits at lists.llvm.org
Thu Aug 6 04:52:17 PDT 2026


https://github.com/abidh updated https://github.com/llvm/llvm-project/pull/214011

>From a2ab81a0fc963f5132c4906171110540bd6e4dd0 Mon Sep 17 00:00:00 2001
From: Abid Qadeer <haqadeer at amd.com>
Date: Fri, 7 Nov 2025 10:45:48 +0000
Subject: [PATCH 1/3] [flang][debug] Add accurate line number information to
 module.

The line number in a module's debug info was a guess: the source line of
the first module member we encountered, minus one. That was only correct
when the first declaration happened to be on the line right after the
MODULE statement, so debuggers usually reported a module at the wrong
line.

The location of `fir.module_debug_imports` is that of the MODULE
statement, so use it instead. That operation is now generated for every
module and not only for those containing a USE statement, so the
location is available in all cases. It is still only generated when
debug info is requested.

Co-authored-by: Cursor <cursoragent at cursor.com>
---
 flang/lib/Lower/Bridge.cpp                    |  6 ++--
 .../lib/Optimizer/Transforms/AddDebugInfo.cpp | 10 ++++--
 .../Integration/debug-module-line-number.f90  | 28 +++++++++++++++
 flang/test/Lower/debug-module-line-info.f90   | 36 +++++++++++++++++++
 4 files changed, 74 insertions(+), 6 deletions(-)
 create mode 100644 flang/test/Integration/debug-module-line-number.f90
 create mode 100644 flang/test/Lower/debug-module-line-info.f90

diff --git a/flang/lib/Lower/Bridge.cpp b/flang/lib/Lower/Bridge.cpp
index ed8b256f47fd4..8b2d3da5d1115 100644
--- a/flang/lib/Lower/Bridge.cpp
+++ b/flang/lib/Lower/Bridge.cpp
@@ -288,15 +288,15 @@ static void emitUseStmtOp(Fortran::lower::AbstractConverter &converter,
                          renamesAttr, hasOnlyWithRenamesAttr);
 }
 
-/// Emit fir.module_debug_imports for USE statements in a module.
+/// Emit fir.module_debug_imports for USE statements in a module. The operation
+/// is emitted for every module, even one with no USE statement, because its
+/// location is also what tells AddDebugInfo the line of the MODULE statement.
 static void
 emitModuleDebugImports(Fortran::lower::AbstractConverter &converter,
                        mlir::OpBuilder &builder, mlir::Location loc,
                        const Fortran::lower::pft::ModuleLikeUnit &mod) {
   if (!converter.getLoweringOptions().getPreserveUseDebugInfo())
     return;
-  if (mod.preservedUseStmts.empty())
-    return;
 
   const Fortran::semantics::Scope &modScope = mod.getScope();
   const Fortran::semantics::Symbol *modSym = modScope.symbol();
diff --git a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
index 7c0b32e48832e..82e9466c0a056 100644
--- a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
+++ b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
@@ -440,6 +440,13 @@ mlir::LLVM::DIModuleAttr AddDebugInfoPass::getOrCreateModuleAttr(
   if (auto iter{moduleMap.find(name)}; iter != moduleMap.end()) {
     modAttr = iter->getValue();
   } else {
+    // A module defined in this compilation unit has a fir.module_debug_imports
+    // whose location is that of the MODULE statement. Prefer it over the
+    // caller's guess, which is derived from a member's declaration.
+    if (auto iter{moduleDebugImportsByName.find(name)};
+        iter != moduleDebugImportsByName.end())
+      line = getLineFromLoc(iter->second.getLoc());
+
     // When decl is true, it means that module is only being used in this
     // compilation unit and it is defined elsewhere. But if the file/line/scope
     // fields are valid, the module is not merged with its definition and is
@@ -478,9 +485,6 @@ AddDebugInfoPass::getModuleAttrFromGlobalOp(fir::GlobalOp globalOp,
   // one). The isInitialized() seems to provide the right information
   // but inverted. It is true where module is actually defined but false where
   // it is used.
-  // FIXME: Currently we don't have the line number on which a module was
-  // declared. We are using a best guess of line - 1 where line is the source
-  // line of the first member of the module that we encounter.
   unsigned line = getLineFromLoc(globalOp.getLoc());
 
   mlir::LLVM::DISubprogramAttr sp =
diff --git a/flang/test/Integration/debug-module-line-number.f90 b/flang/test/Integration/debug-module-line-number.f90
new file mode 100644
index 0000000000000..6c8b2003fd337
--- /dev/null
+++ b/flang/test/Integration/debug-module-line-number.f90
@@ -0,0 +1,28 @@
+! RUN: %flang_fc1 -emit-llvm -debug-info-kind=standalone %s -o - | FileCheck %s
+
+module test_mod
+! CHECK-DAG: !DIModule(scope: !{{.*}}, name: "test_mod", file: !{{.*}}, line: [[@LINE-1]])
+  integer :: mod_var
+contains
+  subroutine test_sub()
+    mod_var = 100
+  end subroutine test_sub
+end module test_mod
+
+module another_mod
+! CHECK-DAG: !DIModule(scope: !{{.*}}, name: "another_mod", file: !{{.*}}, line: [[@LINE-1]])
+  real :: x
+contains
+  function get_value() result(res)
+    real :: res
+    res = 42.0
+  end function get_value
+end module another_mod
+
+program main
+  use test_mod
+  use another_mod
+  call test_sub()
+  x = get_value()
+end program main
+
diff --git a/flang/test/Lower/debug-module-line-info.f90 b/flang/test/Lower/debug-module-line-info.f90
new file mode 100644
index 0000000000000..5876aea170cfd
--- /dev/null
+++ b/flang/test/Lower/debug-module-line-info.f90
@@ -0,0 +1,36 @@
+! The location of fir.module_debug_imports is what tells AddDebugInfo the line
+! of the MODULE statement, so the operation is emitted for every module, even
+! one with no USE statement, and only when full debug info is requested.
+
+! RUN: %flang_fc1 -emit-hlfir -debug-info-kind=standalone %s -o - \
+! RUN:   -mmlir -mlir-print-debuginfo -mmlir -mlir-print-local-scope \
+! RUN:   | FileCheck %s --check-prefix=WITH_DEBUG
+! RUN: %flang_fc1 -emit-hlfir %s -o - | FileCheck %s --check-prefix=NO_DEBUG
+! RUN: %flang_fc1 -emit-hlfir -debug-info-kind=line-tables-only %s -o - \
+! RUN:   | FileCheck %s --check-prefix=NO_DEBUG
+
+! NO_DEBUG-NOT: fir.module_debug_imports
+
+module no_use_mod
+  integer :: mod_var
+contains
+  subroutine test_sub()
+    mod_var = 100
+  end subroutine test_sub
+end module no_use_mod
+! WITH_DEBUG:      fir.module_debug_imports "no_use_mod" {
+! WITH_DEBUG-NEXT: } loc("{{.*}}":[[@LINE-8]]:{{[0-9]+}})
+
+module using_mod
+  use no_use_mod
+  real :: x
+end module using_mod
+! WITH_DEBUG:      fir.module_debug_imports "using_mod" {
+! WITH_DEBUG-NEXT:   fir.use_stmt "no_use_mod"
+! WITH_DEBUG:      } loc("{{.*}}":[[@LINE-6]]:{{[0-9]+}})
+
+program main
+  use using_mod
+  call test_sub()
+  x = 1.0
+end program main

>From 2104cf42668493b9293e3b57968adcafc50b1697 Mon Sep 17 00:00:00 2001
From: Abid Qadeer <haqadeer at amd.com>
Date: Tue, 4 Aug 2026 18:07:53 +0100
Subject: [PATCH 2/3] [flang][debug] Add a test for the line number of a
 module.

Test the AddDebugInfo pass directly: check that the line of a
DIModuleAttr is taken from the location of the module's
fir.module_debug_imports, with and without a USE statement, and that a
module without one still falls back to a guess based on its first
member.

Co-authored-by: Cursor <cursoragent at cursor.com>
---
 flang/test/Transforms/debug-module-line.fir | 44 +++++++++++++++++++++
 1 file changed, 44 insertions(+)
 create mode 100644 flang/test/Transforms/debug-module-line.fir

diff --git a/flang/test/Transforms/debug-module-line.fir b/flang/test/Transforms/debug-module-line.fir
new file mode 100644
index 0000000000000..df0fcd8610910
--- /dev/null
+++ b/flang/test/Transforms/debug-module-line.fir
@@ -0,0 +1,44 @@
+// RUN: fir-opt --add-debug-info --mlir-print-debuginfo %s | FileCheck %s
+
+// The line of a DIModuleAttr comes from the location of the module's
+// `fir.module_debug_imports`, which is that of the MODULE statement. Without
+// one, it falls back to a guess based on the first member of the module.
+
+module {
+  // A module whose MODULE statement is on line 4, well before its first
+  // member on line 20. The region is empty because the module has no USE.
+  fir.module_debug_imports "no_use" {
+  } loc(#loc_no_use)
+  fir.global @_QMno_useEx : i32 {
+    %0 = fir.zero_bits i32
+    fir.has_value %0 : i32
+  } loc(#loc_x)
+
+  // Same, but with a USE statement in the region.
+  fir.module_debug_imports "with_use" {
+    fir.use_stmt "no_use"
+  } loc(#loc_with_use)
+  fir.global @_QMwith_useEy : i32 {
+    %0 = fir.zero_bits i32
+    fir.has_value %0 : i32
+  } loc(#loc_y)
+
+  // No `fir.module_debug_imports`, so the line is guessed from the member.
+  fir.global @_QMlegacyEz : i32 {
+    %0 = fir.zero_bits i32
+    fir.has_value %0 : i32
+  } loc(#loc_z)
+}
+#loc_no_use = loc("mod.f90":4:1)
+#loc_x = loc("mod.f90":20:14)
+#loc_with_use = loc("mod.f90":8:1)
+#loc_y = loc("mod.f90":24:14)
+#loc_z = loc("mod.f90":30:14)
+
+// The MODULE statement line is used, not 19 as guessed from the member.
+// CHECK-DAG: #llvm.di_module<{{.*}}name = "no_use", line = 4>
+
+// CHECK-DAG: #llvm.di_module<{{.*}}name = "with_use", line = 8>
+
+// Fallback when the module has no `fir.module_debug_imports`.
+// CHECK-DAG: #llvm.di_module<{{.*}}name = "legacy", line = 29>

>From d23064af75e1c958355f38f34012e0f3875140ec Mon Sep 17 00:00:00 2001
From: Abid Qadeer <haqadeer at amd.com>
Date: Thu, 6 Aug 2026 12:51:50 +0100
Subject: [PATCH 3/3] Handle review comments.

---
 flang/test/Lower/debug-module-line-info.f90 | 10 +++++-----
 flang/test/Transforms/debug-module-line.fir |  9 +++++++--
 2 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/flang/test/Lower/debug-module-line-info.f90 b/flang/test/Lower/debug-module-line-info.f90
index 5876aea170cfd..f02cc1c1b2486 100644
--- a/flang/test/Lower/debug-module-line-info.f90
+++ b/flang/test/Lower/debug-module-line-info.f90
@@ -11,6 +11,8 @@
 
 ! NO_DEBUG-NOT: fir.module_debug_imports
 
+! WITH_DEBUG:      fir.module_debug_imports "no_use_mod" {
+! WITH_DEBUG-NEXT: } loc("{{.*}}":[[@LINE+1]]:{{[0-9]+}})
 module no_use_mod
   integer :: mod_var
 contains
@@ -18,16 +20,14 @@ subroutine test_sub()
     mod_var = 100
   end subroutine test_sub
 end module no_use_mod
-! WITH_DEBUG:      fir.module_debug_imports "no_use_mod" {
-! WITH_DEBUG-NEXT: } loc("{{.*}}":[[@LINE-8]]:{{[0-9]+}})
 
+! WITH_DEBUG:      fir.module_debug_imports "using_mod" {
+! WITH_DEBUG-NEXT:   fir.use_stmt "no_use_mod"
+! WITH_DEBUG:      } loc("{{.*}}":[[@LINE+1]]:{{[0-9]+}})
 module using_mod
   use no_use_mod
   real :: x
 end module using_mod
-! WITH_DEBUG:      fir.module_debug_imports "using_mod" {
-! WITH_DEBUG-NEXT:   fir.use_stmt "no_use_mod"
-! WITH_DEBUG:      } loc("{{.*}}":[[@LINE-6]]:{{[0-9]+}})
 
 program main
   use using_mod
diff --git a/flang/test/Transforms/debug-module-line.fir b/flang/test/Transforms/debug-module-line.fir
index df0fcd8610910..80e8541b8469e 100644
--- a/flang/test/Transforms/debug-module-line.fir
+++ b/flang/test/Transforms/debug-module-line.fir
@@ -14,9 +14,13 @@ module {
     fir.has_value %0 : i32
   } loc(#loc_x)
 
-  // Same, but with a USE statement in the region.
+  // A module with no members of its own, only used by `with_use` below.
+  fir.module_debug_imports "other_mod" {
+  } loc(#loc_other)
+
+  // Same as `no_use`, but with a USE statement in the region.
   fir.module_debug_imports "with_use" {
-    fir.use_stmt "no_use"
+    fir.use_stmt "other_mod"
   } loc(#loc_with_use)
   fir.global @_QMwith_useEy : i32 {
     %0 = fir.zero_bits i32
@@ -31,6 +35,7 @@ module {
 }
 #loc_no_use = loc("mod.f90":4:1)
 #loc_x = loc("mod.f90":20:14)
+#loc_other = loc("mod.f90":6:1)
 #loc_with_use = loc("mod.f90":8:1)
 #loc_y = loc("mod.f90":24:14)
 #loc_z = loc("mod.f90":30:14)



More information about the flang-commits mailing list