[flang-commits] [flang] [flang][debug] Emit debug info for named constants (PR #213974)

Abid Qadeer via flang-commits flang-commits at lists.llvm.org
Thu Aug 6 03:49:36 PDT 2026


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

>From 7adb7c3dff178bb98ea5f892d40f215f9651c084 Mon Sep 17 00:00:00 2001
From: Abid Qadeer <haqadeer at amd.com>
Date: Tue, 4 Aug 2026 15:45:32 +0100
Subject: [PATCH 1/3] [flang][debug] Emit debug info for named constants

Only globals whose uniqued name deconstructs to NameKind::VARIABLE were
described. A Fortran named constant (PARAMETER) is mangled with EC and
deconstructs to NameKind::CONSTANT, so it got no debug info at all and a
debugger could not evaluate one, whether it was declared in a module or
inside a procedure.

A module constant is described the way a module variable already is,
with a DIGlobalVariable scoped to the DIModule. A constant local to a
procedure is described in the scope of that procedure.

Two related fixes for entities whose global has internal linkage, which
covers both a procedure local constant and a procedure local SAVE
variable that was already being described:

 - isLocalToUnit was hardcoded to false, so these were marked
   DW_AT_external. It now follows the linkage of the global.

 - A linkage name was emitted for them. There is no external symbol for
   a debugger to match against, so it is now omitted, which is also what
   clang does for a function local static.
---
 .../lib/Optimizer/Transforms/AddDebugInfo.cpp | 44 ++++++++++++++++---
 .../test/Integration/debug-local-storage.f90  | 20 +++++++++
 .../test/Transforms/debug-local-constant.fir  | 41 +++++++++++++++++
 .../debug-local-global-storage-1.fir          | 11 +++--
 .../test/Transforms/debug-module-constant.fir | 22 ++++++++++
 5 files changed, 127 insertions(+), 11 deletions(-)
 create mode 100644 flang/test/Integration/debug-local-storage.f90
 create mode 100644 flang/test/Transforms/debug-local-constant.fir
 create mode 100644 flang/test/Transforms/debug-module-constant.fir

diff --git a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
index 7c0b32e48832e..878aee4b5e768 100644
--- a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
+++ b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
@@ -179,11 +179,17 @@ mlir::StringAttr getTargetFunctionName(mlir::MLIRContext *context,
 
 } // namespace
 
-// Check if a global represents a module variable
+// Check if a name belongs to a module rather than to a procedure.
+static bool isModuleLevelName(const fir::NameUniquer::DeconstructedName &name) {
+  return name.procs.empty() && !name.modules.empty();
+}
+
+// Check if a global represents a module variable or a module named constant
 static bool isModuleVariable(fir::GlobalOp globalOp) {
   std::pair result = fir::NameUniquer::deconstruct(globalOp.getSymName());
-  return result.first == fir::NameUniquer::NameKind::VARIABLE &&
-         result.second.procs.empty() && !result.second.modules.empty();
+  return (result.first == fir::NameUniquer::NameKind::VARIABLE ||
+          result.first == fir::NameUniquer::NameKind::CONSTANT) &&
+         isModuleLevelName(result.second);
 }
 
 // Look up DIGlobalVariable from a global symbol
@@ -377,7 +383,8 @@ void AddDebugInfoPass::handleDeclareOp(fir::cg::XDeclareOp declOp,
                                        mlir::Value dummyScope) {
   auto result = fir::NameUniquer::deconstruct(declOp.getUniqName());
 
-  if (result.first != fir::NameUniquer::NameKind::VARIABLE)
+  if (result.first != fir::NameUniquer::NameKind::VARIABLE &&
+      result.first != fir::NameUniquer::NameKind::CONSTANT)
     return;
 
   if (createCommonBlockGlobal(declOp, result.second.name, fileAttr, scopeAttr,
@@ -506,8 +513,23 @@ void AddDebugInfoPass::handleGlobalOp(fir::GlobalOp globalOp,
   mlir::OpBuilder builder(context);
 
   std::pair result = fir::NameUniquer::deconstruct(globalOp.getSymName());
-  if (result.first != fir::NameUniquer::NameKind::VARIABLE)
+  switch (result.first) {
+  case fir::NameUniquer::NameKind::VARIABLE:
+    break;
+  case fir::NameUniquer::NameKind::CONSTANT:
+    // A constant local to a procedure is described while walking that
+    // procedure, where `scope` is its DISubprogramAttr. Reaching here with any
+    // other scope means the procedure is not in the IR, typically because it
+    // was never called and got removed while its constant survived. There is
+    // no procedure to attach the constant to, and describing it at compile
+    // unit scope would wrongly make it visible everywhere.
+    if (!isModuleLevelName(result.second) &&
+        !mlir::isa<mlir::LLVM::DISubprogramAttr>(scope))
+      return;
+    break;
+  default:
     return;
+  }
 
   if (fir::NameUniquer::isSpecialSymbol(result.second.name))
     return;
@@ -518,12 +540,20 @@ void AddDebugInfoPass::handleGlobalOp(fir::GlobalOp globalOp,
   if (modOpt)
     scope = *modOpt;
 
+  // An entity with internal linkage, such as a constant or a SAVE variable
+  // declared inside a procedure, is not visible outside this compilation unit.
+  // It also needs no linkage name because there is no external symbol for a
+  // debugger to match it against.
+  bool isLocalToUnit = globalOp.getLinkName() == "internal";
+  mlir::StringAttr linkageName =
+      isLocalToUnit ? mlir::StringAttr()
+                    : mlir::StringAttr::get(context, globalOp.getName());
+
   mlir::LLVM::DITypeAttr diType =
       typeGen.convertType(globalOp.getType(), fileAttr, scope, declOp);
   auto gvAttr = mlir::LLVM::DIGlobalVariableAttr::get(
       context, scope, mlir::StringAttr::get(context, result.second.name),
-      mlir::StringAttr::get(context, globalOp.getName()), fileAttr, line,
-      diType, /*isLocalToUnit*/ false,
+      linkageName, fileAttr, line, diType, isLocalToUnit,
       /*isDefinition*/ globalOp.isInitialized(), /* alignInBits*/ 0);
   auto dbgExpr = mlir::LLVM::DIGlobalVariableExpressionAttr::get(
       globalOp.getContext(), gvAttr, nullptr);
diff --git a/flang/test/Integration/debug-local-storage.f90 b/flang/test/Integration/debug-local-storage.f90
new file mode 100644
index 0000000000000..71fe2d212f80d
--- /dev/null
+++ b/flang/test/Integration/debug-local-storage.f90
@@ -0,0 +1,20 @@
+! RUN: %flang_fc1 -emit-llvm -debug-info-kind=standalone %s -o - | FileCheck %s
+
+! A named constant and a SAVE variable declared inside a procedure both have
+! internal linkage. They are described in the scope of that procedure, are
+! local to the compile unit and carry no linkage name. The `name` field being
+! followed directly by `scope` is what checks that no linkage name is emitted.
+
+! CHECK-DAG: ![[I4:.*]] = !DIBasicType(name: "integer(kind=4)", size: 32, encoding: DW_ATE_signed)
+! CHECK-DAG: ![[SUB:.*]] = distinct !DISubprogram(name: "counter_fn"{{.*}})
+
+integer function counter_fn()
+! CHECK-DAG: ![[Q:.*]] = distinct !DIGlobalVariable(name: "q", scope: ![[SUB]], file: !{{[0-9]+}}, line: [[@LINE+2]], type: ![[I4]], isLocal: true, isDefinition: true)
+! CHECK-DAG: !DIGlobalVariableExpression(var: ![[Q]], expr: !DIExpression())
+  integer, parameter :: q = 7
+! CHECK-DAG: ![[COUNT:.*]] = distinct !DIGlobalVariable(name: "counter", scope: ![[SUB]], file: !{{[0-9]+}}, line: [[@LINE+2]], type: ![[I4]], isLocal: true, isDefinition: true)
+! CHECK-DAG: !DIGlobalVariableExpression(var: ![[COUNT]], expr: !DIExpression())
+  integer, save :: counter = 0
+  counter = counter + 1
+  counter_fn = q + counter
+end function counter_fn
diff --git a/flang/test/Transforms/debug-local-constant.fir b/flang/test/Transforms/debug-local-constant.fir
new file mode 100644
index 0000000000000..7c70637b1f2a4
--- /dev/null
+++ b/flang/test/Transforms/debug-local-constant.fir
@@ -0,0 +1,41 @@
+// RUN: fir-opt --add-debug-info --mlir-print-debuginfo %s | FileCheck %s
+
+// Test that a named constant (Fortran PARAMETER) declared inside a procedure is
+// described in the scope of that procedure. Two procedures declaring the same
+// name must produce entries in distinct scopes, otherwise a debugger cannot
+// tell them apart. Such a constant has internal linkage, so it is local to the
+// compile unit and carries no linkage name.
+
+module {
+  func.func @_QPone() {
+    %0 = fir.address_of(@_QFoneECq) : !fir.ref<i32>
+    %1 = fircg.ext_declare %0 {uniq_name = "_QFoneECq"} : (!fir.ref<i32>) -> !fir.ref<i32> loc(#loc2)
+    return
+  } loc(#loc1)
+  func.func @_QPtwo() {
+    %0 = fir.address_of(@_QFtwoECq) : !fir.ref<i32>
+    %1 = fircg.ext_declare %0 {uniq_name = "_QFtwoECq"} : (!fir.ref<i32>) -> !fir.ref<i32> loc(#loc4)
+    return
+  } loc(#loc3)
+  fir.global internal @_QFoneECq constant : i32 {
+    %c111_i32 = arith.constant 111 : i32
+    fir.has_value %c111_i32 : i32
+  } loc(#loc2)
+  fir.global internal @_QFtwoECq constant : i32 {
+    %c777_i32 = arith.constant 777 : i32
+    fir.has_value %c777_i32 : i32
+  } loc(#loc4)
+}
+#loc1 = loc("test.f90":1:1)
+#loc2 = loc("test.f90":3:3)
+#loc3 = loc("test.f90":7:1)
+#loc4 = loc("test.f90":9:3)
+
+// CHECK-DAG: #[[I4:.*]] = #llvm.di_basic_type<tag = DW_TAG_base_type, name = "integer(kind=4)", sizeInBits = 32, encoding = DW_ATE_signed>
+// CHECK-DAG: #[[ONE:.*]] = #llvm.di_subprogram<{{.*}}name = "one"{{.*}}>
+// CHECK-DAG: #[[TWO:.*]] = #llvm.di_subprogram<{{.*}}name = "two"{{.*}}>
+
+// Each constant is scoped to its own procedure and, having internal linkage,
+// is local to the unit and has no linkage name.
+// CHECK-DAG: #llvm.di_global_variable<scope = #[[ONE]], name = "q", file = {{.*}}, line = 3, type = #[[I4]], isLocalToUnit = true, isDefined = true>
+// CHECK-DAG: #llvm.di_global_variable<scope = #[[TWO]], name = "q", file = {{.*}}, line = 9, type = #[[I4]], isLocalToUnit = true, isDefined = true>
diff --git a/flang/test/Transforms/debug-local-global-storage-1.fir b/flang/test/Transforms/debug-local-global-storage-1.fir
index 6b9ea5b9dbb2d..4a50626981d6b 100644
--- a/flang/test/Transforms/debug-local-global-storage-1.fir
+++ b/flang/test/Transforms/debug-local-global-storage-1.fir
@@ -47,7 +47,10 @@ module attributes {dlti.dl_spec = #dlti.dl_spec<#dlti.dl_entry<i64, dense<64> :
 // CHECK-DAG: #[[MOD:.*]] = #llvm.di_module<{{.*}}scope = #[[CU]]{{.*}}name = "example"{{.*}}>
 // CHECK-DAG: #[[SP:.*]] = #llvm.di_subprogram<{{.*}}name = "test"{{.*}}>
 // CHECK-DAG: #[[MOD_SP:.*]] = #llvm.di_subprogram<{{.*}}name = "mod_sub"{{.*}}retainedNodes = {{.*}}>
-// CHECK-DAG: #llvm.di_global_variable<scope = #[[SP]], name = "arr"{{.*}}line = 22{{.*}}>
-// CHECK-DAG: #llvm.di_global_variable<scope = #[[SP]], name = "s"{{.*}}line = 23{{.*}}>
-// CHECK-DAG: #llvm.di_global_variable<scope = #[[MOD_SP]], name = "ss"{{.*}}line = 12{{.*}}>
-// CHECK-DAG: #llvm.di_global_variable<scope = #[[MOD]], name = "mod_arr"{{.*}}line = 5{{.*}}>
+// A variable with the SAVE attribute inside a procedure has internal linkage,
+// so it is local to the compile unit and has no linkage name. A module
+// variable is visible to other compilation units and keeps its linkage name.
+// CHECK-DAG: #llvm.di_global_variable<scope = #[[SP]], name = "arr", file = {{.*}}, line = 22, type = {{.*}}, isLocalToUnit = true, isDefined = true>
+// CHECK-DAG: #llvm.di_global_variable<scope = #[[SP]], name = "s", file = {{.*}}, line = 23, type = {{.*}}, isLocalToUnit = true, isDefined = true>
+// CHECK-DAG: #llvm.di_global_variable<scope = #[[MOD_SP]], name = "ss", file = {{.*}}, line = 12, type = {{.*}}, isLocalToUnit = true, isDefined = true>
+// CHECK-DAG: #llvm.di_global_variable<scope = #[[MOD]], name = "mod_arr", linkageName = "_QMexampleEmod_arr"{{.*}}line = 5{{.*}}>
diff --git a/flang/test/Transforms/debug-module-constant.fir b/flang/test/Transforms/debug-module-constant.fir
new file mode 100644
index 0000000000000..0202b7cfa1350
--- /dev/null
+++ b/flang/test/Transforms/debug-module-constant.fir
@@ -0,0 +1,22 @@
+// RUN: fir-opt --add-debug-info --mlir-print-debuginfo %s | FileCheck %s
+
+// Test that a named constant (Fortran PARAMETER) declared in a module is
+// described in the scope of that module.
+
+module {
+  fir.global @_QMhelperECpi constant : f32 {
+    %cst = arith.constant 3.14159274 : f32
+    fir.has_value %cst : f32
+  } loc(#loc1)
+  func.func @_QMhelperPtest() {
+    return
+  } loc(#loc2)
+}
+#loc1 = loc("test.f90":8:26)
+#loc2 = loc("test.f90":12:5)
+
+// CHECK-DAG: #[[R4:.*]] = #llvm.di_basic_type<tag = DW_TAG_base_type, name = "real(kind=4)", sizeInBits = 32, encoding = DW_ATE_float>
+// CHECK-DAG: #[[CU:.*]] = #llvm.di_compile_unit<{{.*}}>
+// CHECK-DAG: #[[MOD:.*]] = #llvm.di_module<{{.*}}scope = #[[CU]], name = "helper"{{.*}}>
+// CHECK-DAG: #[[PI:.*]] = #llvm.di_global_variable<scope = #[[MOD]], name = "pi", linkageName = "_QMhelperECpi"{{.*}}line = 8, type = #[[R4]], isDefined = true>
+// CHECK-DAG: #[[PIE:.*]] = #llvm.di_global_variable_expression<var = #[[PI]]>

>From 678be84c7549deaeb3d8e0b13f721905d6f0a27a Mon Sep 17 00:00:00 2001
From: Abid Qadeer <haqadeer at amd.com>
Date: Wed, 5 Aug 2026 10:57:49 +0100
Subject: [PATCH 2/3] [flang][debug] Address review comments.

Add an integration test for module level named constants.

Rename isModuleVariable to isModuleDataObject. Move the test on the kind
of a name to its own helper.

Co-authored-by: Cursor <cursoragent at cursor.com>
---
 .../lib/Optimizer/Transforms/AddDebugInfo.cpp | 24 ++++++-----
 .../Integration/debug-module-constant.f90     | 42 +++++++++++++++++++
 2 files changed, 56 insertions(+), 10 deletions(-)
 create mode 100644 flang/test/Integration/debug-module-constant.f90

diff --git a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
index 878aee4b5e768..25098835f5039 100644
--- a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
+++ b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
@@ -179,17 +179,22 @@ mlir::StringAttr getTargetFunctionName(mlir::MLIRContext *context,
 
 } // namespace
 
+// Check if a name is that of a data object, which in Fortran is a variable or
+// a named constant.
+static bool isDataObjectName(fir::NameUniquer::NameKind kind) {
+  return kind == fir::NameUniquer::NameKind::VARIABLE ||
+         kind == fir::NameUniquer::NameKind::CONSTANT;
+}
+
 // Check if a name belongs to a module rather than to a procedure.
 static bool isModuleLevelName(const fir::NameUniquer::DeconstructedName &name) {
   return name.procs.empty() && !name.modules.empty();
 }
 
-// Check if a global represents a module variable or a module named constant
-static bool isModuleVariable(fir::GlobalOp globalOp) {
+// Check if a global represents a data object declared in a module.
+static bool isModuleDataObject(fir::GlobalOp globalOp) {
   std::pair result = fir::NameUniquer::deconstruct(globalOp.getSymName());
-  return (result.first == fir::NameUniquer::NameKind::VARIABLE ||
-          result.first == fir::NameUniquer::NameKind::CONSTANT) &&
-         isModuleLevelName(result.second);
+  return isDataObjectName(result.first) && isModuleLevelName(result.second);
 }
 
 // Look up DIGlobalVariable from a global symbol
@@ -383,8 +388,7 @@ void AddDebugInfoPass::handleDeclareOp(fir::cg::XDeclareOp declOp,
                                        mlir::Value dummyScope) {
   auto result = fir::NameUniquer::deconstruct(declOp.getUniqName());
 
-  if (result.first != fir::NameUniquer::NameKind::VARIABLE &&
-      result.first != fir::NameUniquer::NameKind::CONSTANT)
+  if (!isDataObjectName(result.first))
     return;
 
   if (createCommonBlockGlobal(declOp, result.second.name, fileAttr, scopeAttr,
@@ -1067,7 +1071,7 @@ void AddDebugInfoPass::runOnOperation() {
 
   // Process module globals early.
   // Walk through all DeclareOps in functions and process globals that are
-  // module variables. This ensures that when we process USE statements,
+  // module data objects. This ensures that when we process USE statements,
   // the DIGlobalVariable lookups will succeed.
   if (debugLevel == mlir::LLVM::DIEmissionKind::Full) {
     module.walk([&](fir::cg::XDeclareOp declOp) {
@@ -1075,8 +1079,8 @@ void AddDebugInfoPass::runOnOperation() {
       if (defOp && llvm::isa<fir::AddrOfOp>(defOp)) {
         if (auto globalOp =
                 symbolTable.lookup<fir::GlobalOp>(declOp.getUniqName())) {
-          // Only process module variables here, not SAVE variables
-          if (isModuleVariable(globalOp)) {
+          // Only process module data objects here, not SAVE variables
+          if (isModuleDataObject(globalOp)) {
             handleGlobalOp(globalOp, fileAttr, cuAttr, typeGen, &symbolTable,
                            declOp);
           }
diff --git a/flang/test/Integration/debug-module-constant.f90 b/flang/test/Integration/debug-module-constant.f90
new file mode 100644
index 0000000000000..5e48029adc6c8
--- /dev/null
+++ b/flang/test/Integration/debug-module-constant.f90
@@ -0,0 +1,42 @@
+! RUN: %flang_fc1 -emit-llvm -debug-info-kind=standalone %s -o - | FileCheck %s
+! RUN: %flang_fc1 -emit-llvm -debug-info-kind=line-tables-only %s -o - | FileCheck --check-prefix=LINEONLY %s
+
+! A named constant declared in a module is described like a module variable: in
+! the scope of the module, with a linkage name, and visible outside this compile
+! unit.
+
+! CHECK-DAG: ![[FILE:.*]] = !DIFile(filename: {{.*}}debug-module-constant.f90{{.*}})
+! CHECK-DAG: ![[CU:.*]] = distinct !DICompileUnit({{.*}}file: ![[FILE]]{{.*}})
+! CHECK-DAG: ![[MOD:.*]] = !DIModule(scope: ![[CU]], name: "helper"{{.*}})
+! CHECK-DAG: ![[I4:.*]] = !DIBasicType(name: "integer(kind=4)", size: 32, encoding: DW_ATE_signed)
+! CHECK-DAG: ![[R4:.*]] = !DIBasicType(name: "real(kind=4)", size: 32, encoding: DW_ATE_float)
+
+module helper
+! CHECK-DAG: ![[MAX:.*]] = distinct !DIGlobalVariable(name: "max_size", linkageName: "_QMhelperECmax_size", scope: ![[MOD]], file: ![[FILE]], line: [[@LINE+2]], type: ![[I4]], isLocal: false, isDefinition: true)
+! CHECK-DAG: !DIGlobalVariableExpression(var: ![[MAX]], expr: !DIExpression())
+  integer, parameter :: max_size = 100
+
+! CHECK-DAG: ![[PI:.*]] = distinct !DIGlobalVariable(name: "pi", linkageName: "_QMhelperECpi", scope: ![[MOD]], file: ![[FILE]], line: [[@LINE+2]], type: ![[R4]], isLocal: false, isDefinition: true)
+! CHECK-DAG: !DIGlobalVariableExpression(var: ![[PI]], expr: !DIExpression())
+  real, parameter :: pi = 3.14159274
+
+! CHECK-DAG: ![[PRIMES:.*]] = distinct !DIGlobalVariable(name: "primes", linkageName: "_QMhelperECprimes", scope: ![[MOD]], file: ![[FILE]], line: [[@LINE+3]], type: ![[ARR:.*]], isLocal: false, isDefinition: true)
+! CHECK-DAG: ![[ARR]] = !DICompositeType(tag: DW_TAG_array_type, baseType: ![[I4]]{{.*}})
+! CHECK-DAG: !DIGlobalVariableExpression(var: ![[PRIMES]], expr: !DIExpression())
+  integer, parameter :: primes(3) = [2, 3, 5]
+
+! CHECK-DAG: ![[TAG:.*]] = distinct !DIGlobalVariable(name: "tag", linkageName: "_QMhelperECtag", scope: ![[MOD]], file: ![[FILE]], line: [[@LINE+3]], type: ![[STR:.*]], isLocal: false, isDefinition: true)
+! CHECK-DAG: ![[STR]] = !DIStringType(size: 40, encoding: DW_ATE_ASCII)
+! CHECK-DAG: !DIGlobalVariableExpression(var: ![[TAG]], expr: !DIExpression())
+  character(len=5), parameter :: tag = "hello"
+end module helper
+
+program test
+  use helper
+  implicit none
+  integer :: n
+  n = max_size + primes(2)
+  print *, pi, tag, n
+end program test
+
+! LINEONLY-NOT: DIGlobalVariable

>From 9fec84bf793c82f75c01b3fc5e6cb3c2ac109606 Mon Sep 17 00:00:00 2001
From: Abid Qadeer <haqadeer at amd.com>
Date: Thu, 6 Aug 2026 11:49:02 +0100
Subject: [PATCH 3/3] Handle review comments(2).

Add const to a local variable.
---
 flang/lib/Optimizer/Transforms/AddDebugInfo.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
index 25098835f5039..d080f5598aac4 100644
--- a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
+++ b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
@@ -548,7 +548,7 @@ void AddDebugInfoPass::handleGlobalOp(fir::GlobalOp globalOp,
   // declared inside a procedure, is not visible outside this compilation unit.
   // It also needs no linkage name because there is no external symbol for a
   // debugger to match it against.
-  bool isLocalToUnit = globalOp.getLinkName() == "internal";
+  const bool isLocalToUnit = globalOp.getLinkName() == "internal";
   mlir::StringAttr linkageName =
       isLocalToUnit ? mlir::StringAttr()
                     : mlir::StringAttr::get(context, globalOp.getName());



More information about the flang-commits mailing list