[flang-commits] [flang] Reland [flang][debug] Emit debug info for named constants- #213974 (PR #215369)
Abid Qadeer via flang-commits
flang-commits at lists.llvm.org
Tue Sep 1 04:22:54 PDT 2026
https://github.com/abidh updated https://github.com/llvm/llvm-project/pull/215369
>From 790c99c0dc0e7ee78f4a73af8f4c5a56763feeed 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/9] [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 8ab96172349ce..a8c08b70e689d 100644
--- a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
+++ b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
@@ -181,11 +181,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
@@ -379,7 +385,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,
@@ -512,8 +519,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;
@@ -524,12 +546,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 90560c4085d5343f367934493e8c68bf4627a00c 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/9] [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 a8c08b70e689d..ca26fdedc4196 100644
--- a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
+++ b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
@@ -181,17 +181,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
@@ -385,8 +390,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,
@@ -1085,7 +1089,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) {
@@ -1093,8 +1097,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 97e44fdb7ff90997c2197b4313ac22aa8005b15b 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/9] 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 ca26fdedc4196..4e4006a0dccad 100644
--- a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
+++ b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
@@ -554,7 +554,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());
>From 4c2fb234f22f86906377defcbd172640e9a6c12e Mon Sep 17 00:00:00 2001
From: Abid Qadeer <haqadeer at amd.com>
Date: Mon, 10 Aug 2026 17:40:58 +0100
Subject: [PATCH 4/9] [flang][debug] Only describe a named constant this unit
defines
A named constant is described by the address of the global holding its
value. A compilation unit that merely uses the declaring module sees only
a declaration of that global, so the debug info relocates against a
symbol nothing here defines. For a user module the module's own object
supplies the definition, but an intrinsic module has no object file, so
the reference stays undefined and the link fails. This is what the
original commit was reverted for, reported as #214777.
Describe a constant only where this unit defines its global. Constants of
an intrinsic module are consequently not described in the units using
them.
Co-authored-by: Cursor <cursoragent at cursor.com>
---
.../lib/Optimizer/Transforms/AddDebugInfo.cpp | 4 ++++
.../debug-module-constant-relocation.f90 | 18 ++++++++++++++++++
.../debug-module-constant-declaration.fir | 19 +++++++++++++++++++
3 files changed, 41 insertions(+)
create mode 100644 flang/test/Integration/debug-module-constant-relocation.f90
create mode 100644 flang/test/Transforms/debug-module-constant-declaration.fir
diff --git a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
index 4e4006a0dccad..516b254839cde 100644
--- a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
+++ b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
@@ -536,6 +536,10 @@ void AddDebugInfoPass::handleGlobalOp(fir::GlobalOp globalOp,
if (!isModuleLevelName(result.second) &&
!mlir::isa<mlir::LLVM::DISubprogramAttr>(scope))
return;
+ // Don't describe a constant for which we only have a declaration. It could
+ // leave an unresolved symbol in the debug information.
+ if (!globalOp.isInitialized())
+ return;
break;
default:
return;
diff --git a/flang/test/Integration/debug-module-constant-relocation.f90 b/flang/test/Integration/debug-module-constant-relocation.f90
new file mode 100644
index 0000000000000..51ad55cd3fc89
--- /dev/null
+++ b/flang/test/Integration/debug-module-constant-relocation.f90
@@ -0,0 +1,18 @@
+! REQUIRES: x86-registered-target
+
+! RUN: %flang_fc1 -triple x86_64-unknown-linux-gnu -emit-obj -debug-info-kind=standalone %s -o %t.o
+! RUN: llvm-readelf -r %t.o | FileCheck %s
+! RUN: llvm-readelf --symbols %t.o | FileCheck %s --check-prefix=NO_UND
+
+! Test that the object file has no undefined symbol from iso_fortran_env, which
+! means the debug information leaves no unresolved relocation behind.
+
+program p
+ use iso_fortran_env
+ implicit none
+ print *, 'hello'
+end program p
+
+! CHECK: .rela.debug_info
+
+! NO_UND-NOT: UND{{.*}}_QMiso_fortran_env
diff --git a/flang/test/Transforms/debug-module-constant-declaration.fir b/flang/test/Transforms/debug-module-constant-declaration.fir
new file mode 100644
index 0000000000000..385f9a3c0ddf3
--- /dev/null
+++ b/flang/test/Transforms/debug-module-constant-declaration.fir
@@ -0,0 +1,19 @@
+// RUN: fir-opt --add-debug-info --mlir-print-debuginfo %s | FileCheck %s
+// RUN: fir-opt --add-debug-info --mlir-print-debuginfo %s | FileCheck %s --check-prefix=NO_DECL
+
+module {
+ // This constant is defined here and should be described.
+ fir.global @_QMhelperECdefined constant : f32 {
+ %cst = arith.constant 3.14159274 : f32
+ fir.has_value %cst : f32
+ } loc(#loc1)
+ // This constant is only a declaration and should not be described.
+ fir.global @_QMhelperECdeclared constant : f32 loc(#loc2)
+}
+#loc1 = loc("test.f90":8:26)
+#loc2 = loc("test.f90":9:26)
+
+// CHECK-DAG: #[[MOD:.*]] = #llvm.di_module<{{.*}}name = "helper"{{.*}}>
+// CHECK-DAG: #llvm.di_global_variable<scope = #[[MOD]], name = "defined"{{.*}}>
+
+// NO_DECL-NOT: name = "declared"
>From c834dc59d8c51b82f5b1d6a0369091364c2420f8 Mon Sep 17 00:00:00 2001
From: Abid Qadeer <haqadeer at amd.com>
Date: Wed, 12 Aug 2026 12:03:08 +0100
Subject: [PATCH 5/9] [flang][debug] Decide module definedness before building
any DIModule
Whether a module is described as a definition or as a declaration was
inferred from whichever member happened to reach getOrCreateModuleAttr
first. That is wrong twice over. A DIModuleAttr is immutable, so the
first member to mention a module fixes the answer for all the others,
and no single member can supply the answer anyway: using an intrinsic
module materializes named constants of its own in this unit, so the
first such constant would claim the module is defined here. A program
that does nothing but use iso_fortran_env and print a string ended up
describing that module as defined at line 75 of a five line file.
Work the answer out up front instead. Lowering emits one
fir.module_debug_imports for every module and submodule it compiles, and
that set says exactly which modules this unit defines. Submodules are
not described in their own right yet and the entities they define hang
off the DIModuleAttr of the ancestor module, so an ancestor also counts
as defined when a submodule below it is compiled here. Were it left as a
declaration it would carry no scope, and those entities could not reach
a compile unit and would be dropped from the debug information.
Co-authored-by: Cursor <cursoragent at cursor.com>
---
.../lib/Optimizer/Transforms/AddDebugInfo.cpp | 80 ++++++++++++++-----
.../Integration/debug-module-not-defined.f90 | 14 ++++
.../debug-local-global-storage-1.fir | 5 ++
flang/test/Transforms/debug-module-1.fir | 5 ++
.../test/Transforms/debug-module-constant.fir | 5 ++
flang/test/Transforms/debug-module-line.fir | 10 +--
6 files changed, 93 insertions(+), 26 deletions(-)
create mode 100644 flang/test/Integration/debug-module-not-defined.f90
diff --git a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
index 516b254839cde..efeba8ab6c6c0 100644
--- a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
+++ b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
@@ -77,9 +77,13 @@ class AddDebugInfoPass : public fir::impl::AddDebugInfoBase<AddDebugInfoPass> {
/// Maps Fortran module name -> `fir.module_debug_imports`.
llvm::StringMap<fir::ModuleDebugImportsOp> moduleDebugImportsByName;
- mlir::LLVM::DIModuleAttr getOrCreateModuleAttr(
- const std::string &name, mlir::LLVM::DIFileAttr fileAttr,
- mlir::LLVM::DIScopeAttr scope, unsigned line, bool decl);
+ /// Names of the modules whose DIModule this compilation unit defines.
+ llvm::StringSet<> definedModuleNames;
+
+ mlir::LLVM::DIModuleAttr
+ getOrCreateModuleAttr(const std::string &name,
+ mlir::LLVM::DIFileAttr fileAttr,
+ mlir::LLVM::DIScopeAttr scope);
mlir::LLVM::DICommonBlockAttr
getOrCreateCommonBlockAttr(llvm::StringRef name,
mlir::LLVM::DIFileAttr fileAttr,
@@ -109,6 +113,7 @@ class AddDebugInfoPass : public fir::impl::AddDebugInfoBase<AddDebugInfoPass> {
mlir::SymbolTable *symbolTable,
llvm::SetVector<mlir::LLVM::DIImportedEntityAttr> &importedEntities);
void buildModuleDebugImportsMap(mlir::ModuleOp module);
+ void buildDefinedModuleNames(mlir::ModuleOp module);
void expandUseStmtForDebug(
fir::UseStmtOp useOp, mlir::LLVM::DISubprogramAttr spAttr,
mlir::LLVM::DIFileAttr fileAttr, mlir::LLVM::DICompileUnitAttr cuAttr,
@@ -445,17 +450,20 @@ mlir::LLVM::DICommonBlockAttr AddDebugInfoPass::getOrCreateCommonBlockAttr(
// The `module` does not have a first class representation in the `FIR`. We
// extract information about it from the name of the identifiers and keep a
// map to avoid duplication.
-mlir::LLVM::DIModuleAttr AddDebugInfoPass::getOrCreateModuleAttr(
- const std::string &name, mlir::LLVM::DIFileAttr fileAttr,
- mlir::LLVM::DIScopeAttr scope, unsigned line, bool decl) {
+mlir::LLVM::DIModuleAttr
+AddDebugInfoPass::getOrCreateModuleAttr(const std::string &name,
+ mlir::LLVM::DIFileAttr fileAttr,
+ mlir::LLVM::DIScopeAttr scope) {
mlir::MLIRContext *context = &getContext();
mlir::LLVM::DIModuleAttr modAttr;
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.
+ unsigned line = 0;
+ bool decl = !definedModuleNames.contains(name);
+
+ // The location of the fir.module_debug_imports is that of the MODULE
+ // statement. A module that has none is not defined here, and gets no line.
if (auto iter{moduleDebugImportsByName.find(name)};
iter != moduleDebugImportsByName.end())
line = fir::getLineFromLoc(iter->second.getLoc());
@@ -495,20 +503,14 @@ AddDebugInfoPass::getModuleAttrFromGlobalOp(fir::GlobalOp globalOp,
// of a corresponding module body).
// But in practice, compilers use declaration attribute with a module in cases
// where module was defined in another source file (only being used in this
- // 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.
- unsigned line = fir::getLineFromLoc(globalOp.getLoc());
-
+ // one). Whether that is the case here is settled by getOrCreateModuleAttr.
mlir::LLVM::DISubprogramAttr sp =
mlir::dyn_cast_if_present<mlir::LLVM::DISubprogramAttr>(scope);
// Modules are generated at compile unit scope
if (sp)
scope = sp.getCompileUnit();
- return getOrCreateModuleAttr(result.second.modules[0], fileAttr, scope,
- std::max(line - 1, (unsigned)1),
- !globalOp.isInitialized());
+ return getOrCreateModuleAttr(result.second.modules[0], fileAttr, scope);
}
void AddDebugInfoPass::handleGlobalOp(fir::GlobalOp globalOp,
@@ -706,8 +708,7 @@ void AddDebugInfoPass::handleFuncOp(mlir::func::FuncOp funcOp,
}
}
} else if (!result.second.modules.empty()) {
- Scope = getOrCreateModuleAttr(result.second.modules[0], fileAttr, cuAttr,
- line - 1, false);
+ Scope = getOrCreateModuleAttr(result.second.modules[0], fileAttr, cuAttr);
}
auto addTargetOpDISP = [&](bool lineTableOnly,
@@ -971,6 +972,43 @@ void AddDebugInfoPass::buildModuleDebugImportsMap(mlir::ModuleOp module) {
});
}
+// Work out which modules this compilation unit defines. It has to be settled
+// before any DIModuleAttr is built, because the attribute is immutable and the
+// first member to mention a module fixes it for all the others.
+//
+// Lowering emits one fir.module_debug_imports for every module and submodule it
+// compiles, which is what says the module is defined here. No individual member
+// can say it: an array named constant from an intrinsic module, for one, is
+// materialized locally as a linkonce_odr definition in every unit that uses it,
+// although the module is defined in no object file at all.
+void AddDebugInfoPass::buildDefinedModuleNames(mlir::ModuleOp module) {
+ definedModuleNames.clear();
+ for (auto &entry : moduleDebugImportsByName)
+ definedModuleNames.insert(entry.getKey());
+
+ // We do not describe submodules yet: a submodule gets no DIModuleAttr of its
+ // own and the entities it defines hang off the DIModuleAttr of its ancestor
+ // module. So the ancestor has to be a definition in a unit that compiles the
+ // submodule. Were it a declaration, it would carry no scope, and those
+ // entities would not be able to reach a compile unit and would be dropped
+ // from the debug information entirely. The mangled name of a module level
+ // global carries its whole module chain, so mark the ancestor as defined
+ // whenever a submodule below it is compiled here. This keeps the current
+ // behavior until submodules are described in their own right.
+ for (auto globalOp : module.getOps<fir::GlobalOp>()) {
+ std::pair result = fir::NameUniquer::deconstruct(globalOp.getSymName());
+ if (!isModuleLevelName(result.second))
+ continue;
+ llvm::ArrayRef<std::string> modules = result.second.modules;
+ for (const std::string &submodule : modules.drop_front()) {
+ if (moduleDebugImportsByName.contains(submodule)) {
+ definedModuleNames.insert(modules.front());
+ break;
+ }
+ }
+ }
+}
+
void AddDebugInfoPass::expandUseStmtForDebug(
fir::UseStmtOp useOp, mlir::LLVM::DISubprogramAttr spAttr,
mlir::LLVM::DIFileAttr fileAttr, mlir::LLVM::DICompileUnitAttr cuAttr,
@@ -983,8 +1021,7 @@ void AddDebugInfoPass::expandUseStmtForDebug(
mlir::MLIRContext *context = &getContext();
mlir::LLVM::DIModuleAttr modAttr =
- getOrCreateModuleAttr(modName, fileAttr, cuAttr, /*line=*/1,
- /*decl=*/true);
+ getOrCreateModuleAttr(modName, fileAttr, cuAttr);
llvm::SetVector<mlir::LLVM::DIImportedEntityAttr> importedModules;
if (useOp.hasOnlyClause() || useOp.getHasOnlyWithRenames())
@@ -1020,6 +1057,7 @@ void AddDebugInfoPass::runOnOperation() {
mlir::MLIRContext *context = &getContext();
mlir::SymbolTable symbolTable(module);
buildModuleDebugImportsMap(module);
+ buildDefinedModuleNames(module);
llvm::StringRef fileName;
std::string filePath;
std::optional<mlir::DataLayout> dl =
diff --git a/flang/test/Integration/debug-module-not-defined.f90 b/flang/test/Integration/debug-module-not-defined.f90
new file mode 100644
index 0000000000000..e8637bcc88b2b
--- /dev/null
+++ b/flang/test/Integration/debug-module-not-defined.f90
@@ -0,0 +1,14 @@
+! RUN: %flang_fc1 -emit-llvm -debug-info-kind=standalone %s -o - | FileCheck %s
+
+! A module that this compilation unit does not define is described as a
+! declaration. Nothing here compiles iso_fortran_env, so its DIModule must carry
+! no scope, file or line, even though using it materializes named constants of
+! its own in this unit.
+
+program p
+ use iso_fortran_env
+ implicit none
+ print *, 'hello'
+end program p
+
+! CHECK: !DIModule(scope: null, name: "iso_fortran_env", isDecl: true)
diff --git a/flang/test/Transforms/debug-local-global-storage-1.fir b/flang/test/Transforms/debug-local-global-storage-1.fir
index 4a50626981d6b..572727594c323 100644
--- a/flang/test/Transforms/debug-local-global-storage-1.fir
+++ b/flang/test/Transforms/debug-local-global-storage-1.fir
@@ -1,6 +1,10 @@
// RUN: fir-opt --add-debug-info --mlir-print-debuginfo %s | FileCheck %s
module attributes {dlti.dl_spec = #dlti.dl_spec<#dlti.dl_entry<i64, dense<64> : vector<2xi64>>, #dlti.dl_entry<!llvm.ptr<272>, dense<64> : vector<4xi64>>, #dlti.dl_entry<!llvm.ptr<271>, dense<32> : vector<4xi64>>, #dlti.dl_entry<!llvm.ptr<270>, dense<32> : vector<4xi64>>, #dlti.dl_entry<f128, dense<128> : vector<2xi64>>, #dlti.dl_entry<f80, dense<128> : vector<2xi64>>, #dlti.dl_entry<i128, dense<128> : vector<2xi64>>, #dlti.dl_entry<i8, dense<8> : vector<2xi64>>, #dlti.dl_entry<!llvm.ptr, dense<64> : vector<4xi64>>, #dlti.dl_entry<i1, dense<8> : vector<2xi64>>, #dlti.dl_entry<f16, dense<16> : vector<2xi64>>, #dlti.dl_entry<f64, dense<64> : vector<2xi64>>, #dlti.dl_entry<i32, dense<32> : vector<2xi64>>, #dlti.dl_entry<i16, dense<16> : vector<2xi64>>, #dlti.dl_entry<"dlti.stack_alignment", 128 : i64>, #dlti.dl_entry<"dlti.endianness", "little">>, fir.defaultkind = "a1c4d8i4l4r4", fir.kindmap = "", llvm.data_layout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"} {
+ // Marks `example` as defined in this compilation unit, as lowering does for
+ // every module it compiles.
+ fir.module_debug_imports "example" {
+ } loc(#loc0)
func.func @_QMexamplePmod_sub() {
fir.use_stmt "example"
%c2 = arith.constant 2 : index
@@ -36,6 +40,7 @@ module attributes {dlti.dl_spec = #dlti.dl_spec<#dlti.dl_entry<i64, dense<64> :
fir.has_value %c2_i32 : i32
} loc(#loc3)
}
+#loc0 = loc("test.f90":1:1)
#loc1 = loc("test.f90":21:1)
#loc2 = loc("test.f90":22:1)
#loc3 = loc("test.f90":23:1)
diff --git a/flang/test/Transforms/debug-module-1.fir b/flang/test/Transforms/debug-module-1.fir
index 18f4892d60632..f68a164b548ed 100644
--- a/flang/test/Transforms/debug-module-1.fir
+++ b/flang/test/Transforms/debug-module-1.fir
@@ -2,6 +2,10 @@
module {
+ // Marks `helper` as defined in this compilation unit, as lowering does for
+ // every module it compiles.
+ fir.module_debug_imports "helper" {
+ } loc(#loc0)
fir.global @_QMhelperEgli : i32 {
%0 = fir.zero_bits i32
fir.has_value %0 : i32
@@ -20,6 +24,7 @@ module {
return
} loc(#loc3)
}
+#loc0 = loc("test.f90":10:1)
#loc1 = loc("test.f90":12:11)
#loc2 = loc("test.f90":15:8)
#loc3 = loc("test.f90":20:5)
diff --git a/flang/test/Transforms/debug-module-constant.fir b/flang/test/Transforms/debug-module-constant.fir
index 0202b7cfa1350..8f0d052d01a61 100644
--- a/flang/test/Transforms/debug-module-constant.fir
+++ b/flang/test/Transforms/debug-module-constant.fir
@@ -4,6 +4,10 @@
// described in the scope of that module.
module {
+ // Marks `helper` as defined in this compilation unit, as lowering does for
+ // every module it compiles.
+ fir.module_debug_imports "helper" {
+ } loc(#loc0)
fir.global @_QMhelperECpi constant : f32 {
%cst = arith.constant 3.14159274 : f32
fir.has_value %cst : f32
@@ -12,6 +16,7 @@ module {
return
} loc(#loc2)
}
+#loc0 = loc("test.f90":5:1)
#loc1 = loc("test.f90":8:26)
#loc2 = loc("test.f90":12:5)
diff --git a/flang/test/Transforms/debug-module-line.fir b/flang/test/Transforms/debug-module-line.fir
index 80e8541b8469e..e4e1ca407c23e 100644
--- a/flang/test/Transforms/debug-module-line.fir
+++ b/flang/test/Transforms/debug-module-line.fir
@@ -1,8 +1,9 @@
// 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.
+// `fir.module_debug_imports`, which is that of the MODULE statement. A module
+// without one is not defined in this compilation unit, so it is described as a
+// declaration and carries no line at all.
module {
// A module whose MODULE statement is on line 4, well before its first
@@ -27,7 +28,7 @@ module {
fir.has_value %0 : i32
} loc(#loc_y)
- // No `fir.module_debug_imports`, so the line is guessed from the member.
+ // No `fir.module_debug_imports`, so this module is only used here.
fir.global @_QMlegacyEz : i32 {
%0 = fir.zero_bits i32
fir.has_value %0 : i32
@@ -45,5 +46,4 @@ module {
// 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>
+// CHECK-DAG: #llvm.di_module<name = "legacy", isDecl = true>
>From 409ff6b4f76a6dfe7154e44dcf3f921b807ec075 Mon Sep 17 00:00:00 2001
From: Abid Qadeer <haqadeer at amd.com>
Date: Wed, 12 Aug 2026 15:02:21 +0100
Subject: [PATCH 6/9] [flang][debug] Test named constants imported with ONLY
and renaming
A named constant reached through a USE statement takes the same path as a
module variable, but nothing covered it. Extend the use statement test with
a scalar constant, an array constant and a renamed constant in an ONLY list,
and with a constant renamed without ONLY, which lands in the elements array
of the module import rather than in a top level imported declaration.
Co-authored-by: Cursor <cursoragent at cursor.com>
---
flang/test/Integration/debug-use-stmt.f90 | 24 ++++++++++++++++++++++-
1 file changed, 23 insertions(+), 1 deletion(-)
diff --git a/flang/test/Integration/debug-use-stmt.f90 b/flang/test/Integration/debug-use-stmt.f90
index d55c381deadf0..3a8c16a775aae 100644
--- a/flang/test/Integration/debug-use-stmt.f90
+++ b/flang/test/Integration/debug-use-stmt.f90
@@ -2,27 +2,40 @@
module testmod
integer :: var_a = 10, var_b = 20, var_c = 30
+ integer, parameter :: par_a = 11, par_b = 22
+ integer, parameter :: tbl(4) = [1, 2, 3, 4]
end module testmod
module testmod2
real :: var_x = 1.0, var_y = 2.0
end module testmod2
+module testmod3
+ real, parameter :: par_x = 3.5, par_y = 4.5
+end module testmod3
+
program test_use
- use testmod, only: var_b, var_d => var_c
+ use testmod, only: var_b, var_d => var_c, par_a, par_d => par_b, tbl
use testmod2, var_z => var_y
+ use testmod3, par_z => par_y
implicit none
print *, var_b
print *, var_d
print *, var_z
+ print *, par_a, par_d, tbl(2), par_z
end program
! CHECK-DAG: [[TESTMOD:![0-9]+]] = !DIModule(scope: !{{.*}}, name: "testmod"
! CHECK-DAG: [[TESTMOD2:![0-9]+]] = !DIModule(scope: !{{.*}}, name: "testmod2"
+! CHECK-DAG: [[TESTMOD3:![0-9]+]] = !DIModule(scope: !{{.*}}, name: "testmod3"
! CHECK-DAG: [[VAR_B:![0-9]+]] = distinct !DIGlobalVariable(name: "var_b", linkageName: "_QMtestmodEvar_b"
! CHECK-DAG: [[VAR_C:![0-9]+]] = distinct !DIGlobalVariable(name: "var_c", linkageName: "_QMtestmodEvar_c"
! CHECK-DAG: [[VAR_Y:![0-9]+]] = distinct !DIGlobalVariable(name: "var_y", linkageName: "_QMtestmod2Evar_y"
+! CHECK-DAG: [[PAR_A:![0-9]+]] = distinct !DIGlobalVariable(name: "par_a", linkageName: "_QMtestmodECpar_a"
+! CHECK-DAG: [[PAR_B:![0-9]+]] = distinct !DIGlobalVariable(name: "par_b", linkageName: "_QMtestmodECpar_b"
+! CHECK-DAG: [[TBL:![0-9]+]] = distinct !DIGlobalVariable(name: "tbl", linkageName: "_QMtestmodECtbl"
+! CHECK-DAG: [[PAR_Y:![0-9]+]] = distinct !DIGlobalVariable(name: "par_y", linkageName: "_QMtestmod3ECpar_y"
! CHECK-DAG: [[SP:![0-9]+]] = distinct !DISubprogram(name: "test_use", linkageName: "_QQmain"{{.*}}retainedNodes:
@@ -31,6 +44,11 @@ program test_use
! Check testmod imports: var_b directly (no rename), var_d as rename of var_c
! CHECK-DAG: !DIImportedEntity(tag: DW_TAG_imported_declaration, scope: [[SP]], entity: [[VAR_B]],{{.*}}file:{{.*}}line:
! CHECK-DAG: !DIImportedEntity(tag: DW_TAG_imported_declaration, name: "var_d", scope: [[SP]], entity: [[VAR_C]],{{.*}}file:{{.*}}line:
+! A named constant reached through ONLY is imported the same way, whether it is
+! scalar or an array, and whether or not it is renamed.
+! CHECK-DAG: !DIImportedEntity(tag: DW_TAG_imported_declaration, scope: [[SP]], entity: [[PAR_A]],{{.*}}file:{{.*}}line:
+! CHECK-DAG: !DIImportedEntity(tag: DW_TAG_imported_declaration, name: "par_d", scope: [[SP]], entity: [[PAR_B]],{{.*}}file:{{.*}}line:
+! CHECK-DAG: !DIImportedEntity(tag: DW_TAG_imported_declaration, scope: [[SP]], entity: [[TBL]],{{.*}}file:{{.*}}line:
! Check testmod2 import: module imported with rename in elements array
! The module import should have elements containing the var_z rename
@@ -38,3 +56,7 @@ program test_use
! CHECK-DAG: [[ELEMENTS]] = !{[[VAR_Z:![0-9]+]]}
! CHECK-DAG: [[VAR_Z]] = !DIImportedEntity(tag: DW_TAG_imported_declaration, name: "var_z",{{.*}}entity: [[VAR_Y]],
+! A named constant renamed without ONLY lands in the same elements array.
+! CHECK-DAG: [[MOD3_IMPORT:![0-9]+]] = !DIImportedEntity(tag: DW_TAG_imported_module, scope: [[SP]], entity: [[TESTMOD3]],{{.*}}elements: [[ELEMENTS3:![0-9]+]]
+! CHECK-DAG: [[ELEMENTS3]] = !{[[PAR_Z:![0-9]+]]}
+! CHECK-DAG: [[PAR_Z]] = !DIImportedEntity(tag: DW_TAG_imported_declaration, name: "par_z",{{.*}}entity: [[PAR_Y]],
>From 5824428ba75a1645d8793e4dad8c6952c6d3dae7 Mon Sep 17 00:00:00 2001
From: Abid Qadeer <haqadeer at amd.com>
Date: Thu, 20 Aug 2026 12:03:22 +0100
Subject: [PATCH 7/9] [flang][debug] Handle review comments.
Take module definedness from procedures too to handle case where
submodule only contains procedures.
Co-authored-by: Cursor <cursoragent at cursor.com>
---
.../lib/Optimizer/Transforms/AddDebugInfo.cpp | 38 ++++++++++++++-----
.../Integration/debug-submodule-procedure.F90 | 24 ++++++++++++
.../debug-module-submodule-procedure.fir | 20 ++++++++++
3 files changed, 73 insertions(+), 9 deletions(-)
create mode 100644 flang/test/Integration/debug-submodule-procedure.F90
create mode 100644 flang/test/Transforms/debug-module-submodule-procedure.fir
diff --git a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
index efeba8ab6c6c0..7aa85600d7ece 100644
--- a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
+++ b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
@@ -114,6 +114,7 @@ class AddDebugInfoPass : public fir::impl::AddDebugInfoBase<AddDebugInfoPass> {
llvm::SetVector<mlir::LLVM::DIImportedEntityAttr> &importedEntities);
void buildModuleDebugImportsMap(mlir::ModuleOp module);
void buildDefinedModuleNames(mlir::ModuleOp module);
+ void markSubmoduleAncestorsDefined(mlir::ModuleOp module);
void expandUseStmtForDebug(
fir::UseStmtOp useOp, mlir::LLVM::DISubprogramAttr spAttr,
mlir::LLVM::DIFileAttr fileAttr, mlir::LLVM::DICompileUnitAttr cuAttr,
@@ -985,16 +986,21 @@ void AddDebugInfoPass::buildDefinedModuleNames(mlir::ModuleOp module) {
definedModuleNames.clear();
for (auto &entry : moduleDebugImportsByName)
definedModuleNames.insert(entry.getKey());
+ markSubmoduleAncestorsDefined(module);
+}
- // We do not describe submodules yet: a submodule gets no DIModuleAttr of its
- // own and the entities it defines hang off the DIModuleAttr of its ancestor
- // module. So the ancestor has to be a definition in a unit that compiles the
- // submodule. Were it a declaration, it would carry no scope, and those
- // entities would not be able to reach a compile unit and would be dropped
- // from the debug information entirely. The mangled name of a module level
- // global carries its whole module chain, so mark the ancestor as defined
- // whenever a submodule below it is compiled here. This keeps the current
- // behavior until submodules are described in their own right.
+// We do not describe submodules yet: a submodule gets no DIModuleAttr of its
+// own and the entities it defines hang off the DIModuleAttr of its ancestor
+// module. So the ancestor has to be a definition in a unit that compiles the
+// submodule. Were it a declaration, it would carry no scope, and those entities
+// would not be able to reach a compile unit and would be dropped from the debug
+// information entirely. Members of a submodule name it in their mangled name,
+// so take the ancestor from them. All of this goes away once submodules are
+// described in their own right.
+void AddDebugInfoPass::markSubmoduleAncestorsDefined(mlir::ModuleOp module) {
+ // The mangled name of a module level global carries its whole module chain,
+ // so mark the ancestor as defined whenever a submodule below it is compiled
+ // here.
for (auto globalOp : module.getOps<fir::GlobalOp>()) {
std::pair result = fir::NameUniquer::deconstruct(globalOp.getSymName());
if (!isModuleLevelName(result.second))
@@ -1007,6 +1013,20 @@ void AddDebugInfoPass::buildDefinedModuleNames(mlir::ModuleOp module) {
}
}
}
+
+ // Handle a submodule whose members are all procedures, which has no global to
+ // go by. A procedure without a body is defined elsewhere and says nothing
+ // about what this unit defines.
+ for (auto funcOp : module.getOps<mlir::func::FuncOp>()) {
+ if (funcOp.isExternal())
+ continue;
+ mlir::Attribute attr = funcOp->getAttr(fir::getInternalFuncNameAttrName());
+ llvm::StringRef name = attr ? mlir::cast<mlir::StringAttr>(attr).getValue()
+ : funcOp.getName();
+ std::pair result = fir::NameUniquer::deconstruct(name);
+ if (!result.second.modules.empty())
+ definedModuleNames.insert(result.second.modules.front());
+ }
}
void AddDebugInfoPass::expandUseStmtForDebug(
diff --git a/flang/test/Integration/debug-submodule-procedure.F90 b/flang/test/Integration/debug-submodule-procedure.F90
new file mode 100644
index 0000000000000..d8c8fea8a6cf4
--- /dev/null
+++ b/flang/test/Integration/debug-submodule-procedure.F90
@@ -0,0 +1,24 @@
+! RUN: rm -rf %t && mkdir -p %t
+! RUN: %flang_fc1 -fsyntax-only -DSTEP=1 -J%t %s
+! RUN: %flang_fc1 -emit-llvm -debug-info-kind=standalone -J%t %s -o - \
+! RUN: | FileCheck %s
+
+#if STEP == 1
+module subpar
+ implicit none
+ interface
+ module subroutine hello()
+ end subroutine
+ end interface
+end module subpar
+#else
+submodule (subpar) subkid
+contains
+ module subroutine hello()
+ print *, 'hello from submodule'
+ end subroutine hello
+end submodule subkid
+#endif
+
+! CHECK: !DISubprogram(name: "hello", linkageName: "_QMsubparPhello", scope: ![[MOD:[0-9]+]]
+! CHECK: ![[MOD]] = !DIModule(scope: ![[#]], name: "subpar"
diff --git a/flang/test/Transforms/debug-module-submodule-procedure.fir b/flang/test/Transforms/debug-module-submodule-procedure.fir
new file mode 100644
index 0000000000000..db054a913e3a4
--- /dev/null
+++ b/flang/test/Transforms/debug-module-submodule-procedure.fir
@@ -0,0 +1,20 @@
+// RUN: fir-opt --add-debug-info --mlir-print-debuginfo %s | FileCheck %s
+
+module {
+ // Marks `subkid` as compiled here, as lowering does for every submodule.
+ fir.module_debug_imports "subkid" {
+ } loc(#loc0)
+ func.func @_QMsubparPhello() {
+ return
+ } loc(#loc1)
+ func.func private @_QMelsewherePthere() loc(#loc2)
+}
+#loc0 = loc("kid.f90":1:1)
+#loc1 = loc("kid.f90":3:3)
+#loc2 = loc("kid.f90":6:3)
+
+// CHECK-DAG: #[[CU:.*]] = #llvm.di_compile_unit<{{.*}}>
+// CHECK-DAG: #[[SUBPAR:.*]] = #llvm.di_module<{{.*}}scope = #[[CU]], name = "subpar"{{.*}}>
+// CHECK-DAG: #[[ELSEWHERE:.*]] = #llvm.di_module<name = "elsewhere", isDecl = true>
+// CHECK-DAG: #llvm.di_subprogram<{{.*}}scope = #[[SUBPAR]], name = "hello", linkageName = "_QMsubparPhello"{{.*}}>
+// CHECK-DAG: #llvm.di_subprogram<{{.*}}scope = #[[ELSEWHERE]], name = "there", linkageName = "_QMelsewherePthere"{{.*}}>
>From 55ae276c3ac16d6862d5985806e26f50d41e6946 Mon Sep 17 00:00:00 2001
From: Abid Qadeer <haqadeer at amd.com>
Date: Mon, 24 Aug 2026 12:05:09 +0100
Subject: [PATCH 8/9] Fix formatting.
---
flang/lib/Optimizer/Transforms/AddDebugInfo.cpp | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
index 7aa85600d7ece..0847b4d5d3b89 100644
--- a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
+++ b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
@@ -1021,8 +1021,8 @@ void AddDebugInfoPass::markSubmoduleAncestorsDefined(mlir::ModuleOp module) {
if (funcOp.isExternal())
continue;
mlir::Attribute attr = funcOp->getAttr(fir::getInternalFuncNameAttrName());
- llvm::StringRef name = attr ? mlir::cast<mlir::StringAttr>(attr).getValue()
- : funcOp.getName();
+ llvm::StringRef name =
+ attr ? mlir::cast<mlir::StringAttr>(attr).getValue() : funcOp.getName();
std::pair result = fir::NameUniquer::deconstruct(name);
if (!result.second.modules.empty())
definedModuleNames.insert(result.second.modules.front());
>From ab42f5733c6ccc3bb3f3e27ecc1c9798cef4ff2b Mon Sep 17 00:00:00 2001
From: Abid Qadeer <haqadeer at amd.com>
Date: Fri, 28 Aug 2026 11:49:11 +0100
Subject: [PATCH 9/9] [flang][debug] Note named constants in the release notes
Two things here are visible to a user: a named constant can now be printed
in a debugger, and it is described only where its defining unit is
compiled, so constants of an intrinsic module such as iso_fortran_env are
still absent. Record both.
The rest of the change is not worth writing down. Constants now follow the
same scoping rules module and SAVE variables already followed, and the
definedness and submodule handling exist to keep those unchanged.
Co-authored-by: Cursor <cursoragent at cursor.com>
---
flang/docs/ReleaseNotes.md | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/flang/docs/ReleaseNotes.md b/flang/docs/ReleaseNotes.md
index 0815355a86844..c8d6466ac60d6 100644
--- a/flang/docs/ReleaseNotes.md
+++ b/flang/docs/ReleaseNotes.md
@@ -57,6 +57,13 @@ page](https://llvm.org/releases/).
`-mmlir -disable-fir-licm`. The `-mmlir -enable-fir-licm` option that
previously opted into the pass has been removed.
+- Named constants (`PARAMETER`) now appear in the debug information, so a
+ debugger can print them by name. A constant is described only in the
+ compilation unit that defines it: one declared in a module is described
+ where that module is compiled, and one declared in a procedure is local to
+ that unit. Constants of an intrinsic module such as `iso_fortran_env` are
+ not described yet, because no compilation unit defines them.
+
## New Compiler Flags
- Added the gfortran-compatible `-ffpe-trap=` flag, which sets the initial
floating-point exception halting mode of the main program. It takes a
More information about the flang-commits
mailing list