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

via flang-commits flang-commits at lists.llvm.org
Thu Aug 6 05:27:00 PDT 2026


Author: Abid Qadeer
Date: 2026-08-06T13:26:54+01:00
New Revision: a8f0bcbdc4d8c2e046ce107a0a169e310162c807

URL: https://github.com/llvm/llvm-project/commit/a8f0bcbdc4d8c2e046ce107a0a169e310162c807
DIFF: https://github.com/llvm/llvm-project/commit/a8f0bcbdc4d8c2e046ce107a0a169e310162c807.diff

LOG: [flang][debug] Add accurate line number information to module. (#214011)

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>

---------

Co-authored-by: Cursor <cursoragent at cursor.com>

Added: 
    flang/test/Integration/debug-module-line-number.f90
    flang/test/Lower/debug-module-line-info.f90
    flang/test/Transforms/debug-module-line.fir

Modified: 
    flang/lib/Lower/Bridge.cpp
    flang/lib/Optimizer/Transforms/AddDebugInfo.cpp

Removed: 
    


################################################################################
diff  --git a/flang/lib/Lower/Bridge.cpp b/flang/lib/Lower/Bridge.cpp
index 534dffb6f284b..a8e3e4a0aea1a 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 d080f5598aac4..a9a0d74912524 100644
--- a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
+++ b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
@@ -451,6 +451,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
@@ -489,9 +496,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..f02cc1c1b2486
--- /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
+
+! 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
+  subroutine test_sub()
+    mod_var = 100
+  end subroutine test_sub
+end module no_use_mod
+
+! 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
+
+program main
+  use using_mod
+  call test_sub()
+  x = 1.0
+end program main

diff  --git a/flang/test/Transforms/debug-module-line.fir b/flang/test/Transforms/debug-module-line.fir
new file mode 100644
index 0000000000000..80e8541b8469e
--- /dev/null
+++ b/flang/test/Transforms/debug-module-line.fir
@@ -0,0 +1,49 @@
+// 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)
+
+  // 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 "other_mod"
+  } 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_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)
+
+// 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>


        


More information about the flang-commits mailing list