Skip to content

Conversation

@abidh
Copy link
Contributor

@abidh abidh commented Nov 14, 2025

We have a longstanding issue in debug info that use statement is not fully respected. The problem has been described in #160923. This is first part of the effort to address this issue. This PR adds infrastructure to emit use statement information in FIR, which will be used by subsequent patches to generate DWARF debug information.

The information about use statement is collected during semantic analysis and stored in PreservedUseStmt objects. During lowering, fir.use_stmt operations are emitted for each PreservedUseStmt object. The fir.use_stmt operation captures the module name, only list symbols, and any renames specified in the use statement. The fir.use_stmt is removed during CodeGen.

This patch adds infrastructure to emit Fortran USE statement
information in FIR, which will be used by subsequent patches to generate
DWARF debug information.

The information about use statement is collected during semantic
analysis and stored in PreservedUseStmt objects. During
lowering, fir.use_stmt operations are emitted for each PreservedUseStmt
object. The fir.use_stmt operation captures the module name, ONLY list
symbols, and any renames specified in the USE statement.
@llvmbot llvmbot added flang Flang issues not falling into any other category flang:fir-hlfir flang:semantics flang:codegen labels Nov 14, 2025
@llvmbot
Copy link
Member

llvmbot commented Nov 14, 2025

@llvm/pr-subscribers-flang-codegen

@llvm/pr-subscribers-flang-fir-hlfir

Author: Abid Qadeer (abidh)

Changes

We have a longstanding issue in debug info that use statement is not fully respected. The problem has been described in #160923. This is first part of the effort to address this issue. This PR adds infrastructure to emit use statement information in FIR, which will be used by subsequent patches to generate DWARF debug information.

The information about use statement is collected during semantic analysis and stored in PreservedUseStmt objects. During lowering, fir.use_stmt operations are emitted for each PreservedUseStmt object. The fir.use_stmt operation captures the module name, only list symbols, and any renames specified in the use statement. The fir.use_stmt is removed during CodeGen.


Full diff: https://github.com/llvm/llvm-project/pull/168106.diff

7 Files Affected:

  • (modified) flang/include/flang/Optimizer/Dialect/FIRAttr.td (+17)
  • (modified) flang/include/flang/Optimizer/Dialect/FIROps.td (+52)
  • (modified) flang/include/flang/Semantics/scope.h (+25)
  • (modified) flang/lib/Lower/Bridge.cpp (+92)
  • (modified) flang/lib/Optimizer/CodeGen/CodeGen.cpp (+17-2)
  • (modified) flang/lib/Semantics/resolve-names.cpp (+80)
  • (added) flang/test/Lower/debug-use-stmt-symbol-refs.f90 (+59)
diff --git a/flang/include/flang/Optimizer/Dialect/FIRAttr.td b/flang/include/flang/Optimizer/Dialect/FIRAttr.td
index 8a8c60ff0722b..f2723d730ed5b 100644
--- a/flang/include/flang/Optimizer/Dialect/FIRAttr.td
+++ b/flang/include/flang/Optimizer/Dialect/FIRAttr.td
@@ -239,4 +239,21 @@ def fir_FortranInlineAttr
     : EnumAttr<FIROpsDialect, fir_FortranInlineEnum, "inline_attrs"> {
   let assemblyFormat = "`<` $value `>`";
 }
+
+// USE statement rename mapping: local_name => use_name
+def fir_UseRenameAttr : fir_Attr<"UseRename"> {
+  let mnemonic = "use_rename";
+  let summary = "Represents a rename in a Fortran USE statement";
+  let description = [{
+    This attribute stores the mapping for a renamed symbol in a USE statement.
+    For example, in "USE mod, local_var => module_var", this stores the
+    local name and a symbol reference to the module variable.
+  }];
+
+  let parameters = (ins "mlir::StringAttr":$local_name,
+      "mlir::FlatSymbolRefAttr":$symbol);
+
+  let assemblyFormat = "`<` $local_name `,` $symbol `>`";
+}
+
 #endif // FIR_DIALECT_FIR_ATTRS
diff --git a/flang/include/flang/Optimizer/Dialect/FIROps.td b/flang/include/flang/Optimizer/Dialect/FIROps.td
index 289c79bd9b831..9bfe10b27ea8b 100644
--- a/flang/include/flang/Optimizer/Dialect/FIROps.td
+++ b/flang/include/flang/Optimizer/Dialect/FIROps.td
@@ -3054,6 +3054,58 @@ def fir_GlobalLenOp : fir_Op<"global_len", []> {
   }];
 }
 
+def fir_UseStmtOp
+    : fir_Op<"use_stmt", [MemoryEffects<[MemWrite<DebuggingResource>]>]> {
+  let summary = "Represents a Fortran USE statement";
+  let description = [{
+    This operation records a Fortran USE statement with its associated only/rename
+    information. It has no runtime effect but preserves semantic information for
+    debug information generation.
+
+    The operation captures:
+    - The module being used (via module_name string)
+    - Symbol references to symbols imported via the ONLY clause (if present)
+    - Symbol renames (local_name and symbol reference)
+
+    Examples:
+    ```
+      // USE mod1
+      fir.use_stmt "mod1"
+
+      // USE mod1, ONLY: var2
+      fir.use_stmt "mod1" only_symbols [@_QMmod1Evar2]
+
+      // USE mod2, var4 => var3
+      fir.use_stmt "mod2" renames [#fir.use_rename<"var4", @_QMmod2Evar3>]
+
+      // USE mod2, ONLY: var1, renamed => original
+      fir.use_stmt "mod2" only_symbols [@_QMmod2Evar1]
+                   renames [#fir.use_rename<"renamed", @_QMmod2Eoriginal>]
+    ```
+  }];
+
+  let arguments = (ins StrAttr:$module_name,
+      OptionalAttr<ArrayAttr>:$only_symbols, OptionalAttr<ArrayAttr>:$renames);
+
+  let assemblyFormat = [{
+    $module_name
+    (`only_symbols` `[` $only_symbols^ `]`)?
+    (`renames` `[` $renames^ `]`)?
+    attr-dict
+  }];
+
+  let extraClassDeclaration = [{
+    /// Returns true if this is a USE with ONLY clause
+    bool hasOnlyClause() { return getOnlySymbols().has_value(); }
+
+    /// Returns true if this has any renames
+    bool hasRenames() { return getRenames().has_value(); }
+
+    /// Returns true if this imports the entire module (no ONLY clause)
+    bool importsAll() { return !hasOnlyClause(); }
+  }];
+}
+
 def ImplicitFirTerminator : SingleBlockImplicitTerminator<"FirEndOp">;
 
 def fir_TypeInfoOp : fir_Op<"type_info",
diff --git a/flang/include/flang/Semantics/scope.h b/flang/include/flang/Semantics/scope.h
index ecffdb468bf6c..586659781491b 100644
--- a/flang/include/flang/Semantics/scope.h
+++ b/flang/include/flang/Semantics/scope.h
@@ -55,6 +55,19 @@ struct EquivalenceObject {
 };
 using EquivalenceSet = std::vector<EquivalenceObject>;
 
+// Preserved USE statement information for debug info generation.
+struct PreservedUseStmt {
+  enum class Kind { UseOnly, UseRenames, UseAll };
+
+  std::string moduleName;
+  Kind kind;
+  std::vector<std::string> onlyNames; // For Kind::UseOnly
+  std::vector<std::string> renames; // local_name (resolved via GetUltimate)
+
+  PreservedUseStmt(std::string modName, Kind k)
+      : moduleName(std::move(modName)), kind(k) {}
+};
+
 class Scope {
   using mapType = std::map<SourceName, MutableSymbolRef>;
 
@@ -190,6 +203,17 @@ class Scope {
     return equivalenceSets_;
   }
   void add_equivalenceSet(EquivalenceSet &&);
+
+  // Access preserved USE statements for debug info generation
+  std::list<PreservedUseStmt> &preservedUseStmts() {
+    return preservedUseStmts_;
+  }
+  const std::list<PreservedUseStmt> &preservedUseStmts() const {
+    return preservedUseStmts_;
+  }
+  void add_preservedUseStmt(PreservedUseStmt &&stmt) {
+    preservedUseStmts_.push_back(std::move(stmt));
+  }
   // Cray pointers are saved as map of pointee name -> pointer symbol
   const mapType &crayPointers() const { return crayPointers_; }
   void add_crayPointer(const SourceName &, Symbol &);
@@ -301,6 +325,7 @@ class Scope {
   mapType commonBlocks_;
   mapType commonBlockUses_; // USE-assocated COMMON blocks
   std::list<EquivalenceSet> equivalenceSets_;
+  std::list<PreservedUseStmt> preservedUseStmts_;
   mapType crayPointers_;
   std::map<SourceName, common::Reference<Scope>> submodules_;
   std::list<DeclTypeSpec> declTypeSpecs_;
diff --git a/flang/lib/Lower/Bridge.cpp b/flang/lib/Lower/Bridge.cpp
index 5779bcd5d293c..fd0a866c2abac 100644
--- a/flang/lib/Lower/Bridge.cpp
+++ b/flang/lib/Lower/Bridge.cpp
@@ -226,6 +226,95 @@ static mlir::FlatSymbolRefAttr gatherComponentInit(
   return mlir::FlatSymbolRefAttr::get(mlirContext, name);
 }
 
+/// Emit fir.use_stmt operations for USE statements in the given scope
+static void
+emitUseStatementsFromScope(Fortran::lower::AbstractConverter &converter,
+                           mlir::OpBuilder &builder, mlir::Location loc,
+                           const Fortran::semantics::Scope &scope) {
+  mlir::MLIRContext *context = builder.getContext();
+
+  for (const auto &preservedStmt : scope.preservedUseStmts()) {
+
+    auto getMangledName = [&](const std::string &localName) -> std::string {
+      Fortran::parser::CharBlock charBlock{localName.data(), localName.size()};
+      const auto *sym = scope.FindSymbol(charBlock);
+      if (!sym)
+        return "";
+
+      const auto &ultimateSym = sym->GetUltimate();
+
+      // Skip cases which can cause mangleName to fail.
+      if (ultimateSym.has<Fortran::semantics::DerivedTypeDetails>())
+        return "";
+
+      if (const auto *generic =
+              ultimateSym.detailsIf<Fortran::semantics::GenericDetails>()) {
+        if (!generic->specific())
+          return "";
+      }
+
+      return converter.mangleName(ultimateSym);
+    };
+
+    mlir::StringAttr moduleNameAttr =
+        mlir::StringAttr::get(context, preservedStmt.moduleName);
+
+    llvm::SmallVector<mlir::Attribute> onlySymbolAttrs;
+    llvm::SmallVector<mlir::Attribute> renameAttrs;
+
+    switch (preservedStmt.kind) {
+    case Fortran::semantics::PreservedUseStmt::Kind::UseOnly:
+      // USE mod, ONLY: list
+      for (const auto &name : preservedStmt.onlyNames) {
+        std::string mangledName = getMangledName(name);
+        if (!mangledName.empty())
+          onlySymbolAttrs.push_back(
+              mlir::FlatSymbolRefAttr::get(context, mangledName));
+      }
+      // Handle renames within ONLY clause
+      for (const auto &local : preservedStmt.renames) {
+        std::string mangledName = getMangledName(local);
+        if (!mangledName.empty()) {
+          auto localAttr = mlir::StringAttr::get(context, local);
+          auto symbolRef = mlir::FlatSymbolRefAttr::get(context, mangledName);
+          renameAttrs.push_back(
+              fir::UseRenameAttr::get(context, localAttr, symbolRef));
+        }
+      }
+      break;
+
+    case Fortran::semantics::PreservedUseStmt::Kind::UseRenames:
+      // USE mod, renames (import all with some renames)
+      for (const auto &local : preservedStmt.renames) {
+        std::string mangledName = getMangledName(local);
+        if (!mangledName.empty()) {
+          auto localAttr = mlir::StringAttr::get(context, local);
+          auto symbolRef = mlir::FlatSymbolRefAttr::get(context, mangledName);
+          renameAttrs.push_back(
+              fir::UseRenameAttr::get(context, localAttr, symbolRef));
+        }
+      }
+      break;
+
+    case Fortran::semantics::PreservedUseStmt::Kind::UseAll:
+      // USE mod (import all, no renames)
+      break;
+    }
+
+    // Create optional array attributes
+    mlir::ArrayAttr onlySymbolsAttr =
+        onlySymbolAttrs.empty()
+            ? mlir::ArrayAttr()
+            : mlir::ArrayAttr::get(context, onlySymbolAttrs);
+    mlir::ArrayAttr renamesAttr =
+        renameAttrs.empty() ? mlir::ArrayAttr()
+                            : mlir::ArrayAttr::get(context, renameAttrs);
+
+    fir::UseStmtOp::create(builder, loc, moduleNameAttr, onlySymbolsAttr,
+                           renamesAttr);
+  }
+}
+
 /// Helper class to generate the runtime type info global data and the
 /// fir.type_info operations that contain the dipatch tables (if any).
 /// The type info global data is required to describe the derived type to the
@@ -6126,6 +6215,9 @@ class FirConverter : public Fortran::lower::AbstractConverter {
 
     mapDummiesAndResults(funit, callee);
 
+    // Emit USE statement operations for debug info generation
+    emitUseStatementsFromScope(*this, *builder, toLocation(), funit.getScope());
+
     // Map host associated symbols from parent procedure if any.
     if (funit.parentHasHostAssoc())
       funit.parentHostAssoc().internalProcedureBindings(*this, localSymbols);
diff --git a/flang/lib/Optimizer/CodeGen/CodeGen.cpp b/flang/lib/Optimizer/CodeGen/CodeGen.cpp
index ca4aefb653d2a..b92726a50d125 100644
--- a/flang/lib/Optimizer/CodeGen/CodeGen.cpp
+++ b/flang/lib/Optimizer/CodeGen/CodeGen.cpp
@@ -3446,6 +3446,20 @@ struct NoReassocOpConversion : public fir::FIROpConversion<fir::NoReassocOp> {
   }
 };
 
+/// Erase `fir.use_stmt` operations during LLVM lowering.
+/// These operations are only used for debug info generation by the
+/// AddDebugInfo pass and have no runtime representation.
+struct UseStmtOpConversion : public fir::FIROpConversion<fir::UseStmtOp> {
+  using FIROpConversion::FIROpConversion;
+
+  llvm::LogicalResult
+  matchAndRewrite(fir::UseStmtOp useStmt, OpAdaptor adaptor,
+                  mlir::ConversionPatternRewriter &rewriter) const override {
+    rewriter.eraseOp(useStmt);
+    return mlir::success();
+  }
+};
+
 static void genCondBrOp(mlir::Location loc, mlir::Value cmp, mlir::Block *dest,
                         std::optional<mlir::ValueRange> destOps,
                         mlir::ConversionPatternRewriter &rewriter,
@@ -4429,8 +4443,9 @@ void fir::populateFIRToLLVMConversionPatterns(
       SliceOpConversion, StoreOpConversion, StringLitOpConversion,
       SubcOpConversion, TypeDescOpConversion, TypeInfoOpConversion,
       UnboxCharOpConversion, UnboxProcOpConversion, UndefOpConversion,
-      UnreachableOpConversion, XArrayCoorOpConversion, XEmboxOpConversion,
-      XReboxOpConversion, ZeroOpConversion>(converter, options);
+      UnreachableOpConversion, UseStmtOpConversion, XArrayCoorOpConversion,
+      XEmboxOpConversion, XReboxOpConversion, ZeroOpConversion>(converter,
+                                                                options);
 
   // Patterns that are populated without a type converter do not trigger
   // target materializations for the operands of the root op.
diff --git a/flang/lib/Semantics/resolve-names.cpp b/flang/lib/Semantics/resolve-names.cpp
index 09ec951a422ca..85b3b36329471 100644
--- a/flang/lib/Semantics/resolve-names.cpp
+++ b/flang/lib/Semantics/resolve-names.cpp
@@ -3638,6 +3638,86 @@ void ModuleVisitor::Post(const parser::UseStmt &x) {
   for (const auto &[name, symbol] : useModuleScope_->commonBlockUses()) {
     currScope().AddCommonBlockUse(name, symbol->attrs(), symbol->GetUltimate());
   }
+
+  // Preserve USE statement information for debug info generation
+  std::string moduleName{x.moduleName.source.ToString()};
+
+  if (const auto *onlyList{std::get_if<std::list<parser::Only>>(&x.u)}) {
+    // USE mod, ONLY: list
+    PreservedUseStmt stmt{moduleName, PreservedUseStmt::Kind::UseOnly};
+
+    for (const auto &only : *onlyList) {
+      common::visit(
+          common::visitors{
+              [&](const parser::Rename &rename) {
+                // ONLY with rename: ONLY: local => use
+                common::visit(common::visitors{
+                                  [&](const parser::Rename::Names &names) {
+                                    std::string localName{
+                                        std::get<0>(names.t).source.ToString()};
+                                    stmt.renames.push_back(localName);
+                                  },
+                                  [&](const parser::Rename::Operators &) {
+                                    // Operator renames - not commonly needed
+                                    // for debug info
+                                  },
+                              },
+                    rename.u);
+              },
+              [&](const parser::Name &name) {
+                // ONLY without rename: ONLY: name
+                stmt.onlyNames.push_back(name.source.ToString());
+              },
+              [&](const common::Indirection<parser::GenericSpec> &genericSpec) {
+                // Generic spec can contain a Name (for regular symbols) or
+                // operators
+                common::visit(common::visitors{
+                                  [&](const parser::Name &name) {
+                                    stmt.onlyNames.push_back(
+                                        name.source.ToString());
+                                  },
+                                  [&](const auto &) {
+                                    // Operators and special forms - not
+                                    // commonly needed for variable debug info
+                                  },
+                              },
+                    genericSpec.value().u);
+              },
+          },
+          only.u);
+    }
+
+    currScope().add_preservedUseStmt(std::move(stmt));
+  } else if (const auto *renameList{
+                 std::get_if<std::list<parser::Rename>>(&x.u)}) {
+    // USE mod with optional renames (not ONLY)
+    if (renameList->empty()) {
+      // USE mod (import all, no renames)
+      PreservedUseStmt stmt{moduleName, PreservedUseStmt::Kind::UseAll};
+      currScope().add_preservedUseStmt(std::move(stmt));
+    } else {
+      // USE mod, renames (import all with some renames)
+      PreservedUseStmt stmt{moduleName, PreservedUseStmt::Kind::UseRenames};
+
+      for (const auto &rename : *renameList) {
+        common::visit(common::visitors{
+                          [&](const parser::Rename::Names &names) {
+                            std::string localName{
+                                std::get<0>(names.t).source.ToString()};
+                            stmt.renames.push_back(localName);
+                          },
+                          [&](const parser::Rename::Operators &) {
+                            // Operator renames - not commonly needed for debug
+                            // info
+                          },
+                      },
+            rename.u);
+      }
+
+      currScope().add_preservedUseStmt(std::move(stmt));
+    }
+  }
+
   useModuleScope_ = nullptr;
 }
 
diff --git a/flang/test/Lower/debug-use-stmt-symbol-refs.f90 b/flang/test/Lower/debug-use-stmt-symbol-refs.f90
new file mode 100644
index 0000000000000..0f2c1db58740b
--- /dev/null
+++ b/flang/test/Lower/debug-use-stmt-symbol-refs.f90
@@ -0,0 +1,59 @@
+! RUN: bbc -emit-fir %s -o - | FileCheck %s
+
+! Test USE statement lowering to fir.use_stmt operations
+! Covers: USE ONLY, USE with renames, and USE (all)
+
+module mod1
+  integer :: a = 10, b = 20, c = 30
+end module mod1
+
+module mod2
+  real :: x = 1.0, y = 2.0, z = 3.0
+end module mod2
+
+module mod3
+  logical :: flag = .true.
+end module mod3
+
+! Test 1: Program with USE ONLY and USE with renames
+program test_main
+  use mod1, only: b, c
+  use mod2, renamed_y => y
+  implicit none
+  print *, b, c, renamed_y
+end program
+
+! Test 2: Subroutine with USE (all) and different renames
+subroutine test_sub()
+  use mod1
+  use mod2, only: x
+  use mod3, my_flag => flag
+  implicit none
+  print *, a, b, c, x, my_flag
+end subroutine
+
+! Test 3: Function with multiple USE patterns
+function test_func() result(res)
+  use mod1, only: a
+  use mod2, renamed_x => x, renamed_z => z
+  use mod3
+  implicit none
+  integer :: res
+  res = a
+end function
+
+! CHECK-LABEL: func.func @_QQmain()
+! CHECK-DAG: fir.use_stmt "mod1" only_symbols{{\[}}[@_QMmod1Eb, @_QMmod1Ec]]
+! CHECK-DAG: fir.use_stmt "mod2" renames{{\[}}[#fir.use_rename<"renamed_y", @_QMmod2Ey>]]
+
+! CHECK-LABEL: func.func @_QPtest_sub()
+! CHECK-DAG: fir.use_stmt "mod1"{{$}}
+! CHECK-DAG: fir.use_stmt "mod2" only_symbols{{\[}}[@_QMmod2Ex]]
+! CHECK-DAG: fir.use_stmt "mod3" renames{{\[}}[#fir.use_rename<"my_flag", @_QMmod3Eflag>]]
+
+! CHECK-LABEL: func.func @_QPtest_func()
+! CHECK-DAG: fir.use_stmt "mod1" only_symbols{{\[}}[@_QMmod1Ea]]
+! CHECK-DAG: fir.use_stmt "mod2" renames{{\[}}[#fir.use_rename<"renamed_x", @_QMmod2Ex>, #fir.use_rename<"renamed_z", @_QMmod2Ez>]]
+! CHECK-DAG: fir.use_stmt "mod3"{{$}}
+
+

@llvmbot
Copy link
Member

llvmbot commented Nov 14, 2025

@llvm/pr-subscribers-flang-semantics

Author: Abid Qadeer (abidh)

Changes

We have a longstanding issue in debug info that use statement is not fully respected. The problem has been described in #160923. This is first part of the effort to address this issue. This PR adds infrastructure to emit use statement information in FIR, which will be used by subsequent patches to generate DWARF debug information.

The information about use statement is collected during semantic analysis and stored in PreservedUseStmt objects. During lowering, fir.use_stmt operations are emitted for each PreservedUseStmt object. The fir.use_stmt operation captures the module name, only list symbols, and any renames specified in the use statement. The fir.use_stmt is removed during CodeGen.


Full diff: https://github.com/llvm/llvm-project/pull/168106.diff

7 Files Affected:

  • (modified) flang/include/flang/Optimizer/Dialect/FIRAttr.td (+17)
  • (modified) flang/include/flang/Optimizer/Dialect/FIROps.td (+52)
  • (modified) flang/include/flang/Semantics/scope.h (+25)
  • (modified) flang/lib/Lower/Bridge.cpp (+92)
  • (modified) flang/lib/Optimizer/CodeGen/CodeGen.cpp (+17-2)
  • (modified) flang/lib/Semantics/resolve-names.cpp (+80)
  • (added) flang/test/Lower/debug-use-stmt-symbol-refs.f90 (+59)
diff --git a/flang/include/flang/Optimizer/Dialect/FIRAttr.td b/flang/include/flang/Optimizer/Dialect/FIRAttr.td
index 8a8c60ff0722b..f2723d730ed5b 100644
--- a/flang/include/flang/Optimizer/Dialect/FIRAttr.td
+++ b/flang/include/flang/Optimizer/Dialect/FIRAttr.td
@@ -239,4 +239,21 @@ def fir_FortranInlineAttr
     : EnumAttr<FIROpsDialect, fir_FortranInlineEnum, "inline_attrs"> {
   let assemblyFormat = "`<` $value `>`";
 }
+
+// USE statement rename mapping: local_name => use_name
+def fir_UseRenameAttr : fir_Attr<"UseRename"> {
+  let mnemonic = "use_rename";
+  let summary = "Represents a rename in a Fortran USE statement";
+  let description = [{
+    This attribute stores the mapping for a renamed symbol in a USE statement.
+    For example, in "USE mod, local_var => module_var", this stores the
+    local name and a symbol reference to the module variable.
+  }];
+
+  let parameters = (ins "mlir::StringAttr":$local_name,
+      "mlir::FlatSymbolRefAttr":$symbol);
+
+  let assemblyFormat = "`<` $local_name `,` $symbol `>`";
+}
+
 #endif // FIR_DIALECT_FIR_ATTRS
diff --git a/flang/include/flang/Optimizer/Dialect/FIROps.td b/flang/include/flang/Optimizer/Dialect/FIROps.td
index 289c79bd9b831..9bfe10b27ea8b 100644
--- a/flang/include/flang/Optimizer/Dialect/FIROps.td
+++ b/flang/include/flang/Optimizer/Dialect/FIROps.td
@@ -3054,6 +3054,58 @@ def fir_GlobalLenOp : fir_Op<"global_len", []> {
   }];
 }
 
+def fir_UseStmtOp
+    : fir_Op<"use_stmt", [MemoryEffects<[MemWrite<DebuggingResource>]>]> {
+  let summary = "Represents a Fortran USE statement";
+  let description = [{
+    This operation records a Fortran USE statement with its associated only/rename
+    information. It has no runtime effect but preserves semantic information for
+    debug information generation.
+
+    The operation captures:
+    - The module being used (via module_name string)
+    - Symbol references to symbols imported via the ONLY clause (if present)
+    - Symbol renames (local_name and symbol reference)
+
+    Examples:
+    ```
+      // USE mod1
+      fir.use_stmt "mod1"
+
+      // USE mod1, ONLY: var2
+      fir.use_stmt "mod1" only_symbols [@_QMmod1Evar2]
+
+      // USE mod2, var4 => var3
+      fir.use_stmt "mod2" renames [#fir.use_rename<"var4", @_QMmod2Evar3>]
+
+      // USE mod2, ONLY: var1, renamed => original
+      fir.use_stmt "mod2" only_symbols [@_QMmod2Evar1]
+                   renames [#fir.use_rename<"renamed", @_QMmod2Eoriginal>]
+    ```
+  }];
+
+  let arguments = (ins StrAttr:$module_name,
+      OptionalAttr<ArrayAttr>:$only_symbols, OptionalAttr<ArrayAttr>:$renames);
+
+  let assemblyFormat = [{
+    $module_name
+    (`only_symbols` `[` $only_symbols^ `]`)?
+    (`renames` `[` $renames^ `]`)?
+    attr-dict
+  }];
+
+  let extraClassDeclaration = [{
+    /// Returns true if this is a USE with ONLY clause
+    bool hasOnlyClause() { return getOnlySymbols().has_value(); }
+
+    /// Returns true if this has any renames
+    bool hasRenames() { return getRenames().has_value(); }
+
+    /// Returns true if this imports the entire module (no ONLY clause)
+    bool importsAll() { return !hasOnlyClause(); }
+  }];
+}
+
 def ImplicitFirTerminator : SingleBlockImplicitTerminator<"FirEndOp">;
 
 def fir_TypeInfoOp : fir_Op<"type_info",
diff --git a/flang/include/flang/Semantics/scope.h b/flang/include/flang/Semantics/scope.h
index ecffdb468bf6c..586659781491b 100644
--- a/flang/include/flang/Semantics/scope.h
+++ b/flang/include/flang/Semantics/scope.h
@@ -55,6 +55,19 @@ struct EquivalenceObject {
 };
 using EquivalenceSet = std::vector<EquivalenceObject>;
 
+// Preserved USE statement information for debug info generation.
+struct PreservedUseStmt {
+  enum class Kind { UseOnly, UseRenames, UseAll };
+
+  std::string moduleName;
+  Kind kind;
+  std::vector<std::string> onlyNames; // For Kind::UseOnly
+  std::vector<std::string> renames; // local_name (resolved via GetUltimate)
+
+  PreservedUseStmt(std::string modName, Kind k)
+      : moduleName(std::move(modName)), kind(k) {}
+};
+
 class Scope {
   using mapType = std::map<SourceName, MutableSymbolRef>;
 
@@ -190,6 +203,17 @@ class Scope {
     return equivalenceSets_;
   }
   void add_equivalenceSet(EquivalenceSet &&);
+
+  // Access preserved USE statements for debug info generation
+  std::list<PreservedUseStmt> &preservedUseStmts() {
+    return preservedUseStmts_;
+  }
+  const std::list<PreservedUseStmt> &preservedUseStmts() const {
+    return preservedUseStmts_;
+  }
+  void add_preservedUseStmt(PreservedUseStmt &&stmt) {
+    preservedUseStmts_.push_back(std::move(stmt));
+  }
   // Cray pointers are saved as map of pointee name -> pointer symbol
   const mapType &crayPointers() const { return crayPointers_; }
   void add_crayPointer(const SourceName &, Symbol &);
@@ -301,6 +325,7 @@ class Scope {
   mapType commonBlocks_;
   mapType commonBlockUses_; // USE-assocated COMMON blocks
   std::list<EquivalenceSet> equivalenceSets_;
+  std::list<PreservedUseStmt> preservedUseStmts_;
   mapType crayPointers_;
   std::map<SourceName, common::Reference<Scope>> submodules_;
   std::list<DeclTypeSpec> declTypeSpecs_;
diff --git a/flang/lib/Lower/Bridge.cpp b/flang/lib/Lower/Bridge.cpp
index 5779bcd5d293c..fd0a866c2abac 100644
--- a/flang/lib/Lower/Bridge.cpp
+++ b/flang/lib/Lower/Bridge.cpp
@@ -226,6 +226,95 @@ static mlir::FlatSymbolRefAttr gatherComponentInit(
   return mlir::FlatSymbolRefAttr::get(mlirContext, name);
 }
 
+/// Emit fir.use_stmt operations for USE statements in the given scope
+static void
+emitUseStatementsFromScope(Fortran::lower::AbstractConverter &converter,
+                           mlir::OpBuilder &builder, mlir::Location loc,
+                           const Fortran::semantics::Scope &scope) {
+  mlir::MLIRContext *context = builder.getContext();
+
+  for (const auto &preservedStmt : scope.preservedUseStmts()) {
+
+    auto getMangledName = [&](const std::string &localName) -> std::string {
+      Fortran::parser::CharBlock charBlock{localName.data(), localName.size()};
+      const auto *sym = scope.FindSymbol(charBlock);
+      if (!sym)
+        return "";
+
+      const auto &ultimateSym = sym->GetUltimate();
+
+      // Skip cases which can cause mangleName to fail.
+      if (ultimateSym.has<Fortran::semantics::DerivedTypeDetails>())
+        return "";
+
+      if (const auto *generic =
+              ultimateSym.detailsIf<Fortran::semantics::GenericDetails>()) {
+        if (!generic->specific())
+          return "";
+      }
+
+      return converter.mangleName(ultimateSym);
+    };
+
+    mlir::StringAttr moduleNameAttr =
+        mlir::StringAttr::get(context, preservedStmt.moduleName);
+
+    llvm::SmallVector<mlir::Attribute> onlySymbolAttrs;
+    llvm::SmallVector<mlir::Attribute> renameAttrs;
+
+    switch (preservedStmt.kind) {
+    case Fortran::semantics::PreservedUseStmt::Kind::UseOnly:
+      // USE mod, ONLY: list
+      for (const auto &name : preservedStmt.onlyNames) {
+        std::string mangledName = getMangledName(name);
+        if (!mangledName.empty())
+          onlySymbolAttrs.push_back(
+              mlir::FlatSymbolRefAttr::get(context, mangledName));
+      }
+      // Handle renames within ONLY clause
+      for (const auto &local : preservedStmt.renames) {
+        std::string mangledName = getMangledName(local);
+        if (!mangledName.empty()) {
+          auto localAttr = mlir::StringAttr::get(context, local);
+          auto symbolRef = mlir::FlatSymbolRefAttr::get(context, mangledName);
+          renameAttrs.push_back(
+              fir::UseRenameAttr::get(context, localAttr, symbolRef));
+        }
+      }
+      break;
+
+    case Fortran::semantics::PreservedUseStmt::Kind::UseRenames:
+      // USE mod, renames (import all with some renames)
+      for (const auto &local : preservedStmt.renames) {
+        std::string mangledName = getMangledName(local);
+        if (!mangledName.empty()) {
+          auto localAttr = mlir::StringAttr::get(context, local);
+          auto symbolRef = mlir::FlatSymbolRefAttr::get(context, mangledName);
+          renameAttrs.push_back(
+              fir::UseRenameAttr::get(context, localAttr, symbolRef));
+        }
+      }
+      break;
+
+    case Fortran::semantics::PreservedUseStmt::Kind::UseAll:
+      // USE mod (import all, no renames)
+      break;
+    }
+
+    // Create optional array attributes
+    mlir::ArrayAttr onlySymbolsAttr =
+        onlySymbolAttrs.empty()
+            ? mlir::ArrayAttr()
+            : mlir::ArrayAttr::get(context, onlySymbolAttrs);
+    mlir::ArrayAttr renamesAttr =
+        renameAttrs.empty() ? mlir::ArrayAttr()
+                            : mlir::ArrayAttr::get(context, renameAttrs);
+
+    fir::UseStmtOp::create(builder, loc, moduleNameAttr, onlySymbolsAttr,
+                           renamesAttr);
+  }
+}
+
 /// Helper class to generate the runtime type info global data and the
 /// fir.type_info operations that contain the dipatch tables (if any).
 /// The type info global data is required to describe the derived type to the
@@ -6126,6 +6215,9 @@ class FirConverter : public Fortran::lower::AbstractConverter {
 
     mapDummiesAndResults(funit, callee);
 
+    // Emit USE statement operations for debug info generation
+    emitUseStatementsFromScope(*this, *builder, toLocation(), funit.getScope());
+
     // Map host associated symbols from parent procedure if any.
     if (funit.parentHasHostAssoc())
       funit.parentHostAssoc().internalProcedureBindings(*this, localSymbols);
diff --git a/flang/lib/Optimizer/CodeGen/CodeGen.cpp b/flang/lib/Optimizer/CodeGen/CodeGen.cpp
index ca4aefb653d2a..b92726a50d125 100644
--- a/flang/lib/Optimizer/CodeGen/CodeGen.cpp
+++ b/flang/lib/Optimizer/CodeGen/CodeGen.cpp
@@ -3446,6 +3446,20 @@ struct NoReassocOpConversion : public fir::FIROpConversion<fir::NoReassocOp> {
   }
 };
 
+/// Erase `fir.use_stmt` operations during LLVM lowering.
+/// These operations are only used for debug info generation by the
+/// AddDebugInfo pass and have no runtime representation.
+struct UseStmtOpConversion : public fir::FIROpConversion<fir::UseStmtOp> {
+  using FIROpConversion::FIROpConversion;
+
+  llvm::LogicalResult
+  matchAndRewrite(fir::UseStmtOp useStmt, OpAdaptor adaptor,
+                  mlir::ConversionPatternRewriter &rewriter) const override {
+    rewriter.eraseOp(useStmt);
+    return mlir::success();
+  }
+};
+
 static void genCondBrOp(mlir::Location loc, mlir::Value cmp, mlir::Block *dest,
                         std::optional<mlir::ValueRange> destOps,
                         mlir::ConversionPatternRewriter &rewriter,
@@ -4429,8 +4443,9 @@ void fir::populateFIRToLLVMConversionPatterns(
       SliceOpConversion, StoreOpConversion, StringLitOpConversion,
       SubcOpConversion, TypeDescOpConversion, TypeInfoOpConversion,
       UnboxCharOpConversion, UnboxProcOpConversion, UndefOpConversion,
-      UnreachableOpConversion, XArrayCoorOpConversion, XEmboxOpConversion,
-      XReboxOpConversion, ZeroOpConversion>(converter, options);
+      UnreachableOpConversion, UseStmtOpConversion, XArrayCoorOpConversion,
+      XEmboxOpConversion, XReboxOpConversion, ZeroOpConversion>(converter,
+                                                                options);
 
   // Patterns that are populated without a type converter do not trigger
   // target materializations for the operands of the root op.
diff --git a/flang/lib/Semantics/resolve-names.cpp b/flang/lib/Semantics/resolve-names.cpp
index 09ec951a422ca..85b3b36329471 100644
--- a/flang/lib/Semantics/resolve-names.cpp
+++ b/flang/lib/Semantics/resolve-names.cpp
@@ -3638,6 +3638,86 @@ void ModuleVisitor::Post(const parser::UseStmt &x) {
   for (const auto &[name, symbol] : useModuleScope_->commonBlockUses()) {
     currScope().AddCommonBlockUse(name, symbol->attrs(), symbol->GetUltimate());
   }
+
+  // Preserve USE statement information for debug info generation
+  std::string moduleName{x.moduleName.source.ToString()};
+
+  if (const auto *onlyList{std::get_if<std::list<parser::Only>>(&x.u)}) {
+    // USE mod, ONLY: list
+    PreservedUseStmt stmt{moduleName, PreservedUseStmt::Kind::UseOnly};
+
+    for (const auto &only : *onlyList) {
+      common::visit(
+          common::visitors{
+              [&](const parser::Rename &rename) {
+                // ONLY with rename: ONLY: local => use
+                common::visit(common::visitors{
+                                  [&](const parser::Rename::Names &names) {
+                                    std::string localName{
+                                        std::get<0>(names.t).source.ToString()};
+                                    stmt.renames.push_back(localName);
+                                  },
+                                  [&](const parser::Rename::Operators &) {
+                                    // Operator renames - not commonly needed
+                                    // for debug info
+                                  },
+                              },
+                    rename.u);
+              },
+              [&](const parser::Name &name) {
+                // ONLY without rename: ONLY: name
+                stmt.onlyNames.push_back(name.source.ToString());
+              },
+              [&](const common::Indirection<parser::GenericSpec> &genericSpec) {
+                // Generic spec can contain a Name (for regular symbols) or
+                // operators
+                common::visit(common::visitors{
+                                  [&](const parser::Name &name) {
+                                    stmt.onlyNames.push_back(
+                                        name.source.ToString());
+                                  },
+                                  [&](const auto &) {
+                                    // Operators and special forms - not
+                                    // commonly needed for variable debug info
+                                  },
+                              },
+                    genericSpec.value().u);
+              },
+          },
+          only.u);
+    }
+
+    currScope().add_preservedUseStmt(std::move(stmt));
+  } else if (const auto *renameList{
+                 std::get_if<std::list<parser::Rename>>(&x.u)}) {
+    // USE mod with optional renames (not ONLY)
+    if (renameList->empty()) {
+      // USE mod (import all, no renames)
+      PreservedUseStmt stmt{moduleName, PreservedUseStmt::Kind::UseAll};
+      currScope().add_preservedUseStmt(std::move(stmt));
+    } else {
+      // USE mod, renames (import all with some renames)
+      PreservedUseStmt stmt{moduleName, PreservedUseStmt::Kind::UseRenames};
+
+      for (const auto &rename : *renameList) {
+        common::visit(common::visitors{
+                          [&](const parser::Rename::Names &names) {
+                            std::string localName{
+                                std::get<0>(names.t).source.ToString()};
+                            stmt.renames.push_back(localName);
+                          },
+                          [&](const parser::Rename::Operators &) {
+                            // Operator renames - not commonly needed for debug
+                            // info
+                          },
+                      },
+            rename.u);
+      }
+
+      currScope().add_preservedUseStmt(std::move(stmt));
+    }
+  }
+
   useModuleScope_ = nullptr;
 }
 
diff --git a/flang/test/Lower/debug-use-stmt-symbol-refs.f90 b/flang/test/Lower/debug-use-stmt-symbol-refs.f90
new file mode 100644
index 0000000000000..0f2c1db58740b
--- /dev/null
+++ b/flang/test/Lower/debug-use-stmt-symbol-refs.f90
@@ -0,0 +1,59 @@
+! RUN: bbc -emit-fir %s -o - | FileCheck %s
+
+! Test USE statement lowering to fir.use_stmt operations
+! Covers: USE ONLY, USE with renames, and USE (all)
+
+module mod1
+  integer :: a = 10, b = 20, c = 30
+end module mod1
+
+module mod2
+  real :: x = 1.0, y = 2.0, z = 3.0
+end module mod2
+
+module mod3
+  logical :: flag = .true.
+end module mod3
+
+! Test 1: Program with USE ONLY and USE with renames
+program test_main
+  use mod1, only: b, c
+  use mod2, renamed_y => y
+  implicit none
+  print *, b, c, renamed_y
+end program
+
+! Test 2: Subroutine with USE (all) and different renames
+subroutine test_sub()
+  use mod1
+  use mod2, only: x
+  use mod3, my_flag => flag
+  implicit none
+  print *, a, b, c, x, my_flag
+end subroutine
+
+! Test 3: Function with multiple USE patterns
+function test_func() result(res)
+  use mod1, only: a
+  use mod2, renamed_x => x, renamed_z => z
+  use mod3
+  implicit none
+  integer :: res
+  res = a
+end function
+
+! CHECK-LABEL: func.func @_QQmain()
+! CHECK-DAG: fir.use_stmt "mod1" only_symbols{{\[}}[@_QMmod1Eb, @_QMmod1Ec]]
+! CHECK-DAG: fir.use_stmt "mod2" renames{{\[}}[#fir.use_rename<"renamed_y", @_QMmod2Ey>]]
+
+! CHECK-LABEL: func.func @_QPtest_sub()
+! CHECK-DAG: fir.use_stmt "mod1"{{$}}
+! CHECK-DAG: fir.use_stmt "mod2" only_symbols{{\[}}[@_QMmod2Ex]]
+! CHECK-DAG: fir.use_stmt "mod3" renames{{\[}}[#fir.use_rename<"my_flag", @_QMmod3Eflag>]]
+
+! CHECK-LABEL: func.func @_QPtest_func()
+! CHECK-DAG: fir.use_stmt "mod1" only_symbols{{\[}}[@_QMmod1Ea]]
+! CHECK-DAG: fir.use_stmt "mod2" renames{{\[}}[#fir.use_rename<"renamed_x", @_QMmod2Ex>, #fir.use_rename<"renamed_z", @_QMmod2Ez>]]
+! CHECK-DAG: fir.use_stmt "mod3"{{$}}
+
+

Copy link
Contributor

@jeanPerier jeanPerier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @abidh, thanks for working on this.

I wonder if this representation should better be outside of the func.func.

There is a case that is not addressed here I think (at least in the test), which are the indirect accesses when a module itself has use statements.
In some modern application, there may be many of these indirect accesses, and putting all the use as operation in the func.func would pollute the IR quite a lot.

@abidh
Copy link
Contributor Author

abidh commented Nov 21, 2025

Hi @abidh, thanks for working on this.

I wonder if this representation should better be outside of the func.func.

There is a case that is not addressed here I think (at least in the test), which are the indirect accesses when a module itself has use statements. In some modern application, there may be many of these indirect accesses, and putting all the use as operation in the func.func would pollute the IR quite a lot.

Hi @jeanPerier

Thanks for taking a look. I was aware of module level use statements. I intentionally ignored them for this PR for the reason I list below. With this PR, we generate fir.use_stmt only for use statements that are at function level. For example, for the following code, we only get one fir.use_stmt in _QQmain

module base_module
  integer :: base_var_a = 10
end module base_module

module middle_module
  use base_module
  integer :: middle_var = 100
contains
  subroutine middle_sub()
    print *, "middle_sub:", base_var_a, middle_var
  end subroutine middle_sub
end module middle_module

program test
  use middle_module
  print *, "main:", base_var_a, middle_var
  call middle_sub()
end program test

My reasons for not generating fir.use_stmt at module level were the following:

  1. We don't have any FIR module level construct to nest them in so where exactly they should be generated.
  2. Even if we generate them, we will not be able use them to generate DWARF. The LLVM debug metadata like DIModule will require changes.

So I took the simpler approach of generating FIR for only those use statements that are in functions for this PR. That is the common use case and we can generate DWARF for them.

We can generate fir.use_stmt for module level use statement if we want in a future PR. The information about them is now available in lowering code.

Copy link
Member

@ergawy ergawy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Abid. Just a few comments from side.


// Preserved USE statement information for debug info generation.
struct PreservedUseStmt {
enum class Kind { UseOnly, UseRenames, UseAll };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if we need that enum. UseOnly and UseRenames are not mutually exclusive. And UseAll can be deduced if onlyNames is not empty. Also, I don't think we need to keep track of UseAll since during lowering, we do nothing with it.

Suggested change
enum class Kind { UseOnly, UseRenames, UseAll };

Comment on lines +265 to +302
switch (preservedStmt.kind) {
case Fortran::semantics::PreservedUseStmt::Kind::UseOnly:
// USE mod, ONLY: list
for (const auto &name : preservedStmt.onlyNames) {
std::string mangledName = getMangledName(name);
if (!mangledName.empty())
onlySymbolAttrs.push_back(
mlir::FlatSymbolRefAttr::get(context, mangledName));
}
// Handle renames within ONLY clause
for (const auto &local : preservedStmt.renames) {
std::string mangledName = getMangledName(local);
if (!mangledName.empty()) {
auto localAttr = mlir::StringAttr::get(context, local);
auto symbolRef = mlir::FlatSymbolRefAttr::get(context, mangledName);
renameAttrs.push_back(
fir::UseRenameAttr::get(context, localAttr, symbolRef));
}
}
break;

case Fortran::semantics::PreservedUseStmt::Kind::UseRenames:
// USE mod, renames (import all with some renames)
for (const auto &local : preservedStmt.renames) {
std::string mangledName = getMangledName(local);
if (!mangledName.empty()) {
auto localAttr = mlir::StringAttr::get(context, local);
auto symbolRef = mlir::FlatSymbolRefAttr::get(context, mangledName);
renameAttrs.push_back(
fir::UseRenameAttr::get(context, localAttr, symbolRef));
}
}
break;

case Fortran::semantics::PreservedUseStmt::Kind::UseAll:
// USE mod (import all, no renames)
break;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we simplify things a bit as follows?

Suggested change
switch (preservedStmt.kind) {
case Fortran::semantics::PreservedUseStmt::Kind::UseOnly:
// USE mod, ONLY: list
for (const auto &name : preservedStmt.onlyNames) {
std::string mangledName = getMangledName(name);
if (!mangledName.empty())
onlySymbolAttrs.push_back(
mlir::FlatSymbolRefAttr::get(context, mangledName));
}
// Handle renames within ONLY clause
for (const auto &local : preservedStmt.renames) {
std::string mangledName = getMangledName(local);
if (!mangledName.empty()) {
auto localAttr = mlir::StringAttr::get(context, local);
auto symbolRef = mlir::FlatSymbolRefAttr::get(context, mangledName);
renameAttrs.push_back(
fir::UseRenameAttr::get(context, localAttr, symbolRef));
}
}
break;
case Fortran::semantics::PreservedUseStmt::Kind::UseRenames:
// USE mod, renames (import all with some renames)
for (const auto &local : preservedStmt.renames) {
std::string mangledName = getMangledName(local);
if (!mangledName.empty()) {
auto localAttr = mlir::StringAttr::get(context, local);
auto symbolRef = mlir::FlatSymbolRefAttr::get(context, mangledName);
renameAttrs.push_back(
fir::UseRenameAttr::get(context, localAttr, symbolRef));
}
}
break;
case Fortran::semantics::PreservedUseStmt::Kind::UseAll:
// USE mod (import all, no renames)
break;
}
// USE mod, ONLY: list
for (const auto &name : preservedStmt.onlyNames) {
std::string mangledName = getMangledName(name);
if (!mangledName.empty())
onlySymbolAttrs.push_back(
mlir::FlatSymbolRefAttr::get(context, mangledName));
}
// Handle renames
for (const auto &local : preservedStmt.renames) {
std::string mangledName = getMangledName(local);
if (!mangledName.empty()) {
auto localAttr = mlir::StringAttr::get(context, local);
auto symbolRef = mlir::FlatSymbolRefAttr::get(context, mangledName);
renameAttrs.push_back(
fir::UseRenameAttr::get(context, localAttr, symbolRef));
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

flang:codegen flang:fir-hlfir flang:semantics flang Flang issues not falling into any other category

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants