[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
Wed Aug 12 05:24:19 PDT 2026
https://github.com/abidh updated https://github.com/llvm/llvm-project/pull/215369
>From 8977617d72aabf40049d48664d4b8d34d2321338 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/5] [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 82e9466c0a056..f074d574aaffd 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,
@@ -510,8 +517,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;
@@ -522,12 +544,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 0553086b8cdfd32354f473047e5305eebfed735a 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/5] [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 f074d574aaffd..23765235f0aa2 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,
@@ -1071,7 +1075,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) {
@@ -1079,8 +1083,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 611758f0724282490d4eec5452e1e7e4f5739410 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/5] 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 23765235f0aa2..a9a0d74912524 100644
--- a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
+++ b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
@@ -552,7 +552,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 d04f1727449b42a7e76ee5ddc2da5e3967a3372e 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/5] [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 a9a0d74912524..339e27e3b5ec4 100644
--- a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
+++ b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
@@ -534,6 +534,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 5209e7cd5349f5dccc6dab200917ec85529c6e35 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/5] [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 339e27e3b5ec4..1a935ccc0d0a9 100644
--- a/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
+++ b/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp
@@ -75,9 +75,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,
@@ -107,6 +111,7 @@ class AddDebugInfoPass : public fir::impl::AddDebugInfoBase<AddDebugInfoPass> {
mlir::SymbolTable *symbolTable,
llvm::DenseSet<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,
@@ -443,17 +448,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 = getLineFromLoc(iter->second.getLoc());
@@ -493,20 +501,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 = 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,
@@ -702,8 +704,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,
@@ -965,6 +966,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,
@@ -977,8 +1015,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::DenseSet<mlir::LLVM::DIImportedEntityAttr> importedModules;
if (useOp.hasOnlyClause() || useOp.getHasOnlyWithRenames())
@@ -1014,6 +1051,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>
More information about the flang-commits
mailing list