[llvm] Implement support for NSDI DebugFunctionDefinition. (PR #211853)

Manuel Carrasco via llvm-commits llvm-commits at lists.llvm.org
Mon Aug 3 04:09:22 PDT 2026


https://github.com/mgcarrasco updated https://github.com/llvm/llvm-project/pull/211853

>From 94a2f2906f132501f67eaf639a31fe54039d6da2 Mon Sep 17 00:00:00 2001
From: Manuel Carrasco <Manuel.Carrasco at amd.com>
Date: Fri, 24 Jul 2026 04:42:51 -0500
Subject: [PATCH 01/16] Implement support for NSDI DebugFunction opcode.

---
 llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
index d0b7f67866acb..903a424335111 100644
--- a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
@@ -1038,6 +1038,12 @@ void SPIRVNonSemanticDebugHandler::emitNonSemanticDebugStrings(
     emitAndCacheScopePathOpStringReg(TD->getFile(), MAI);
   }
 
+  for (const DISubprogram *SP : SubprogramDefinitions) {
+    emitOpStringIfNew(SP->getName(), MAI);
+    emitOpStringIfNew(SP->getLinkageName(), MAI);
+    emitAndCacheScopePathOpStringReg(SP, MAI);
+  }
+
   for (const auto &[GV, _] : GlobalVariableDebugInfoMap) {
     emitOpStringIfNew(GV->getName(), MAI);
     emitOpStringIfNew(GV->getLinkageName(), MAI);

>From d901d0c3256876b6b685589eaa8a342a77cb3599 Mon Sep 17 00:00:00 2001
From: Manuel Carrasco <Manuel.Carrasco at amd.com>
Date: Tue, 28 Jul 2026 08:23:12 -0500
Subject: [PATCH 02/16] Simplify loops.

---
 llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
index 903a424335111..d0b7f67866acb 100644
--- a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
@@ -1038,12 +1038,6 @@ void SPIRVNonSemanticDebugHandler::emitNonSemanticDebugStrings(
     emitAndCacheScopePathOpStringReg(TD->getFile(), MAI);
   }
 
-  for (const DISubprogram *SP : SubprogramDefinitions) {
-    emitOpStringIfNew(SP->getName(), MAI);
-    emitOpStringIfNew(SP->getLinkageName(), MAI);
-    emitAndCacheScopePathOpStringReg(SP, MAI);
-  }
-
   for (const auto &[GV, _] : GlobalVariableDebugInfoMap) {
     emitOpStringIfNew(GV->getName(), MAI);
     emitOpStringIfNew(GV->getLinkageName(), MAI);

>From 01462125bcf37169af086ccab3a0b5073e09617e Mon Sep 17 00:00:00 2001
From: Manuel Carrasco <Manuel.Carrasco at amd.com>
Date: Fri, 24 Jul 2026 04:42:51 -0500
Subject: [PATCH 03/16] Implement support for NSDI DebugFunction opcode.

---
 llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
index d0b7f67866acb..679dc8f4d616e 100644
--- a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
@@ -1038,6 +1038,12 @@ void SPIRVNonSemanticDebugHandler::emitNonSemanticDebugStrings(
     emitAndCacheScopePathOpStringReg(TD->getFile(), MAI);
   }
 
+  for (const DISubprogram *SP : SubprogramDefinitions) {
+    emitOpStringIfNew(SP->getName(), MAI);
+    emitOpStringIfNew(SP->getLinkageName(), MAI);
+    ScopeToPathOpStringReg[SP] = emitOpStringIfNew(getDebugFullPath(SP), MAI);
+  }
+
   for (const auto &[GV, _] : GlobalVariableDebugInfoMap) {
     emitOpStringIfNew(GV->getName(), MAI);
     emitOpStringIfNew(GV->getLinkageName(), MAI);

>From b16f12fe9602a174af6fb4c92c8b4a39e376b37e Mon Sep 17 00:00:00 2001
From: Manuel Carrasco <Manuel.Carrasco at amd.com>
Date: Tue, 28 Jul 2026 08:23:12 -0500
Subject: [PATCH 04/16] Simplify loops.

---
 llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
index 679dc8f4d616e..d0b7f67866acb 100644
--- a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
@@ -1038,12 +1038,6 @@ void SPIRVNonSemanticDebugHandler::emitNonSemanticDebugStrings(
     emitAndCacheScopePathOpStringReg(TD->getFile(), MAI);
   }
 
-  for (const DISubprogram *SP : SubprogramDefinitions) {
-    emitOpStringIfNew(SP->getName(), MAI);
-    emitOpStringIfNew(SP->getLinkageName(), MAI);
-    ScopeToPathOpStringReg[SP] = emitOpStringIfNew(getDebugFullPath(SP), MAI);
-  }
-
   for (const auto &[GV, _] : GlobalVariableDebugInfoMap) {
     emitOpStringIfNew(GV->getName(), MAI);
     emitOpStringIfNew(GV->getLinkageName(), MAI);

>From ade3084022875c2f63839e8b0f2722efd6713884 Mon Sep 17 00:00:00 2001
From: Manuel Carrasco <Manuel.Carrasco at amd.com>
Date: Fri, 24 Jul 2026 04:42:51 -0500
Subject: [PATCH 05/16] Implement support for NSDI DebugFunction opcode.

---
 llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
index d0b7f67866acb..679dc8f4d616e 100644
--- a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
@@ -1038,6 +1038,12 @@ void SPIRVNonSemanticDebugHandler::emitNonSemanticDebugStrings(
     emitAndCacheScopePathOpStringReg(TD->getFile(), MAI);
   }
 
+  for (const DISubprogram *SP : SubprogramDefinitions) {
+    emitOpStringIfNew(SP->getName(), MAI);
+    emitOpStringIfNew(SP->getLinkageName(), MAI);
+    ScopeToPathOpStringReg[SP] = emitOpStringIfNew(getDebugFullPath(SP), MAI);
+  }
+
   for (const auto &[GV, _] : GlobalVariableDebugInfoMap) {
     emitOpStringIfNew(GV->getName(), MAI);
     emitOpStringIfNew(GV->getLinkageName(), MAI);

>From a543d586a928a812a889b2ac10400946a614e9bf Mon Sep 17 00:00:00 2001
From: Manuel Carrasco <Manuel.Carrasco at amd.com>
Date: Fri, 24 Jul 2026 11:45:11 -0500
Subject: [PATCH 06/16] Implement support for NSDI DebugFunctionDefinition.

---
 llvm/lib/Target/SPIRV/SPIRVAsmPrinter.cpp     |  17 +-
 .../SPIRV/SPIRVNonSemanticDebugHandler.cpp    | 166 +++++++++++++++++-
 .../SPIRV/SPIRVNonSemanticDebugHandler.h      |  71 +++++---
 ...ug-function-definition-after-opvariable.ll |  36 ++++
 .../debug-info/debug-function-definition.ll   |  40 +++++
 5 files changed, 300 insertions(+), 30 deletions(-)
 create mode 100644 llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition-after-opvariable.ll
 create mode 100644 llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition.ll

diff --git a/llvm/lib/Target/SPIRV/SPIRVAsmPrinter.cpp b/llvm/lib/Target/SPIRV/SPIRVAsmPrinter.cpp
index 31953bf811982..4c9c0255397cc 100644
--- a/llvm/lib/Target/SPIRV/SPIRVAsmPrinter.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVAsmPrinter.cpp
@@ -322,16 +322,27 @@ void SPIRVAsmPrinter::emitInstruction(const MachineInstr *MI) {
   SPIRV_MC::verifyInstructionPredicates(MI->getOpcode(),
                                         getSubtargetInfo().getFeatureBits());
 
-  if (!MAI->getSkipEmission(MI))
+  bool InstructionEmitted = !MAI->getSkipEmission(MI);
+  if (InstructionEmitted) {
     outputInstruction(MI);
+    if (NSDebugHandler && !isHidden() && MAI)
+      NSDebugHandler->notifyMachineInstructionEmitted(MI, *MF, *MAI);
+  }
 
   // Output OpLabel after OpFunction and OpFunctionParameter in the first MBB.
   const MachineInstr *NextMI = MI->getNextNode();
-  if (!LabeledMBB.contains(MI->getParent()) && isFuncOrHeaderInstr(MI, TII) &&
-      (!NextMI || !isFuncOrHeaderInstr(NextMI, TII))) {
+  bool BlockHasLabel = LabeledMBB.contains(MI->getParent());
+  bool IsFunctionPreambleInstruction = isFuncOrHeaderInstr(MI, TII);
+  bool IsNextInstructionFunctionPreamble =
+      NextMI && isFuncOrHeaderInstr(NextMI, TII);
+  bool ShouldEmitEntryLabel = !BlockHasLabel && IsFunctionPreambleInstruction &&
+                              !IsNextInstructionFunctionPreamble;
+  if (ShouldEmitEntryLabel) {
     assert(MI->getParent()->getNumber() == MF->front().getNumber() &&
            "OpFunction is not in the front MBB of MF");
     emitOpLabel(*MI->getParent());
+    if (NSDebugHandler && !isHidden() && MAI)
+      NSDebugHandler->notifyEntryLabelEmitted(*MF, *MAI);
   }
 }
 
diff --git a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
index 679dc8f4d616e..23674755b2c17 100644
--- a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
@@ -15,6 +15,8 @@
 #include "llvm/ADT/Twine.h"
 #include "llvm/BinaryFormat/Dwarf.h"
 #include "llvm/CodeGen/AsmPrinter.h"
+#include "llvm/CodeGen/MachineFunction.h"
+#include "llvm/CodeGen/MachineInstr.h"
 #include "llvm/IR/DebugInfo.h"
 #include "llvm/IR/DebugInfoMetadata.h"
 #include "llvm/IR/GlobalVariable.h"
@@ -193,6 +195,35 @@ static uint32_t mapCompositeTypeTag(unsigned Tag) {
   }
 }
 
+static const MachineInstr *
+findLastEmittedFunctionOpVariable(const MachineFunction &MF,
+                                  SPIRV::ModuleAnalysisInfo &MAI) {
+
+  // We iterate over the instructions to find the last OpVariable instruction if
+  // any. The following SPIRV rule is used to terminate the traversal earlier:
+  // SPIR-V 2.16.1, Function Structure: "All OpVariable instructions in a
+  // function must be in the first block in the function. These instructions,
+  // together with any intermixed OpLine and OpNoLine instructions, must be the
+  // first instructions in that block."
+  const MachineInstr *LastOpVariable = nullptr;
+  bool SeenOpVariable = false;
+  for (const MachineInstr &MI : MF.front()) {
+    if (MI.getOpcode() == SPIRV::OpVariable) {
+      SeenOpVariable = true;
+      if (!MAI.getSkipEmission(&MI))
+        LastOpVariable = &MI;
+      continue;
+    }
+
+    bool CanInterleaveWithOpVariable =
+        MI.getOpcode() == SPIRV::OpLine || MI.getOpcode() == SPIRV::OpNoLine;
+    if (SeenOpVariable && !CanInterleaveWithOpVariable &&
+        !MAI.getSkipEmission(&MI))
+      break;
+  }
+  return LastOpVariable;
+}
+
 } // namespace
 
 SPIRVNonSemanticDebugHandler::SPIRVNonSemanticDebugHandler(AsmPrinter &AP)
@@ -242,6 +273,7 @@ void SPIRVNonSemanticDebugHandler::beginModule(Module *M) {
   SubprogramDefinitions.clear();
   GlobalVariableDebugInfoMap.clear();
   DebugFunctionDeclarationRegs.clear();
+  DebugFunctionRegs.clear();
   ScopeToPathOpStringReg.clear();
   CUToCompilationUnitDbgReg.clear();
   DebugSourceRegByFileStr.clear();
@@ -250,6 +282,9 @@ void SPIRVNonSemanticDebugHandler::beginModule(Module *M) {
   I32ConstantCache.clear();
   DebugTypeFunctionCache.clear();
   GlobalDIEmitted = false;
+  GlobalNSDIEnabled = false;
+  CurrentMAI = nullptr;
+  CachedExtInstSetReg = MCRegister();
 #ifndef NDEBUG
   NonSemanticOpStringsSectionEmitted = false;
 #endif
@@ -1057,18 +1092,128 @@ void SPIRVNonSemanticDebugHandler::emitNonSemanticDebugStrings(
 #endif
 }
 
-void SPIRVNonSemanticDebugHandler::emitNonSemanticGlobalDebugInfo(
+void SPIRVNonSemanticDebugHandler::emitDebugFunctionDefinition(
+    MCRegister DebugFunctionReg, MCRegister OpFunctionReg,
     SPIRV::ModuleAnalysisInfo &MAI) {
-  if (GlobalDIEmitted || CompileUnits.empty())
+  assert(DebugFunctionReg.isValid() && OpFunctionReg.isValid() &&
+         "DebugFunctionDefinition operands must be valid");
+  MCRegister VoidTypeReg = getOrEmitOpTypeVoidReg(MAI);
+  emitExtInst(SPIRV::NonSemanticExtInst::DebugFunctionDefinition, VoidTypeReg,
+              CachedExtInstSetReg, {DebugFunctionReg, OpFunctionReg}, MAI);
+}
+
+void SPIRVNonSemanticDebugHandler::resetPerFunctionDebugState() {
+  CurrentMF = nullptr;
+  LastFunctionOpVariable = nullptr;
+  DebugFunctionDefinitionEmitted = false;
+}
+
+void SPIRVNonSemanticDebugHandler::preparePerFunctionDebug(
+    const MachineFunction *MF) {
+  resetPerFunctionDebugState();
+  if (!GlobalNSDIEnabled || !CurrentMAI)
     return;
+
+  CurrentMF = MF;
+
+  if (MF->getFunction()
+          .getFnAttribute(SPIRV_BACKEND_SERVICE_FUN_NAME)
+          .isValid())
+    return;
+
+  const DISubprogram *SP = MF->getFunction().getSubprogram();
+  if (!SP || !SP->isDefinition())
+    return;
+
+  // DebugFunctionDefinition is emitted after the last function-level
+  // OpVariable. If there are none, it is emitted after the entry OpLabel.
+  LastFunctionOpVariable = findLastEmittedFunctionOpVariable(*MF, *CurrentMAI);
+}
+
+void SPIRVNonSemanticDebugHandler::tryEmitDebugFunctionDefinition(
+    SPIRV::ModuleAnalysisInfo &MAI) {
+  if (DebugFunctionDefinitionEmitted || !GlobalNSDIEnabled)
+    return;
+
+  assert(CurrentMF && "no current MachineFunction");
+  const Function &F = CurrentMF->getFunction();
+  const DISubprogram *SP = F.getSubprogram();
+  if (!SP || !SP->isDefinition())
+    return;
+
+  auto DFIt = DebugFunctionRegs.find(SP);
+  if (DFIt == DebugFunctionRegs.end())
+    return;
+
+  MCRegister OpFunctionReg = MAI.getGlobalObjReg(&F);
+  if (!OpFunctionReg.isValid())
+    return;
+
+  emitDebugFunctionDefinition(DFIt->second, OpFunctionReg, MAI);
+  DebugFunctionDefinitionEmitted = true;
+}
+
+void SPIRVNonSemanticDebugHandler::beginFunctionImpl(
+    const MachineFunction *MF) {
+  preparePerFunctionDebug(MF);
+}
+
+void SPIRVNonSemanticDebugHandler::endFunctionImpl(const MachineFunction *MF) {
+  (void)MF;
+  resetPerFunctionDebugState();
+}
+
+void SPIRVNonSemanticDebugHandler::notifyMachineInstructionEmitted(
+    const MachineInstr *MI, const MachineFunction &MF,
+    SPIRV::ModuleAnalysisInfo &MAI) {
+  if (!GlobalNSDIEnabled || DebugFunctionDefinitionEmitted)
+    return;
+  assert(CurrentMF == &MF &&
+         "notification does not match the current MachineFunction");
+  if (MI->getParent() != &MF.front())
+    return;
+
+  // If this is the last function-level OpVariable, emit the
+  // DebugFunctionDefinition. Otherwise, we had already done it before right
+  // after the OpLabel.
+  if (MI == LastFunctionOpVariable)
+    tryEmitDebugFunctionDefinition(MAI);
+}
+
+void SPIRVNonSemanticDebugHandler::notifyEntryLabelEmitted(
+    const MachineFunction &MF, SPIRV::ModuleAnalysisInfo &MAI) {
+  if (!GlobalNSDIEnabled || DebugFunctionDefinitionEmitted)
+    return;
+  assert(CurrentMF == &MF &&
+         "notification does not match the current MachineFunction");
+
+  // If there are no function-level OpVariables, emit the
+  // DebugFunctionDefinition. Otherwise, DebugFunctionDefinition is emitted
+  // after the last OpVariable.
+  if (!LastFunctionOpVariable)
+    tryEmitDebugFunctionDefinition(MAI);
+}
+
+bool SPIRVNonSemanticDebugHandler::emitNonSemanticGlobalDebugInfo(
+    SPIRV::ModuleAnalysisInfo &MAI) {
+  if (GlobalDIEmitted)
+    return GlobalNSDIEnabled;
+
   GlobalDIEmitted = true;
 
+  if (CompileUnits.empty()) {
+    GlobalNSDIEnabled = false;
+    return false;
+  }
+
   // Retrieve the ext inst set register allocated by prepareModuleOutput().
   constexpr unsigned NSSet = static_cast<unsigned>(
       SPIRV::InstructionSet::NonSemantic_Shader_DebugInfo_100);
   MCRegister ExtInstSetReg = MAI.getExtInstSetReg(NSSet);
-  if (!ExtInstSetReg.isValid())
-    return; // Extension not available.
+  if (!ExtInstSetReg.isValid()) {
+    GlobalNSDIEnabled = false;
+    return false;
+  }
 
 #ifndef NDEBUG
   assert(NonSemanticOpStringsSectionEmitted &&
@@ -1076,6 +1221,9 @@ void SPIRVNonSemanticDebugHandler::emitNonSemanticGlobalDebugInfo(
          "emitNonSemanticGlobalDebugInfo()");
 #endif
 
+  CurrentMAI = &MAI;
+  CachedExtInstSetReg = ExtInstSetReg;
+
   MCRegister VoidTypeReg = getOrEmitOpTypeVoidReg(MAI);
   MCRegister I32TypeReg = getOrEmitOpTypeInt32Reg(MAI);
 
@@ -1229,13 +1377,19 @@ void SPIRVNonSemanticDebugHandler::emitNonSemanticGlobalDebugInfo(
   }
 
   // Emit DebugFunction for DISubprogram definitions.
-  for (const DISubprogram *SP : SubprogramDefinitions)
-    emitDebugFunction(SP, VoidTypeReg, I32TypeReg, ExtInstSetReg, MAI);
+  for (const DISubprogram *SP : SubprogramDefinitions) {
+    if (auto FnReg =
+            emitDebugFunction(SP, VoidTypeReg, I32TypeReg, ExtInstSetReg, MAI))
+      DebugFunctionRegs[SP] = *FnReg;
+  }
 
   // Emit DebugGlobalVariable for each collected DIGlobalVariable.
   for (const auto &[GV, Info] : GlobalVariableDebugInfoMap)
     emitDebugGlobalVariable(GV, Info, VoidTypeReg, I32TypeReg, ExtInstSetReg,
                             MAI);
+
+  GlobalNSDIEnabled = true;
+  return true;
 }
 
 SmallString<128>
diff --git a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h
index 5da526587df91..b1793cc0db811 100644
--- a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h
+++ b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h
@@ -40,16 +40,15 @@ class SPIRVSubtarget;
 /// the module contains debug info (llvm.dbg.cu).
 ///
 /// Call sequence:
-///   beginModule()                    -- collect compile-unit metadata.
-///   prepareModuleOutput()            -- add extension + ext inst set to MAI.
-///   emitNonSemanticDebugStrings()    -- OpString for NSDI strings (sec. 7).
-///   emitNonSemanticGlobalDebugInfo() -- emit DebugSource,
-///                                       DebugCompilationUnit, DebugTypeBasic,
-///                                       DebugTypePointer, DebugTypeFunction,
-///                                       DebugFunctionDeclaration,
-///                                       DebugFunction.
-///   beginFunctionImpl()              -- no-op (no per-function DI yet).
-///   endFunctionImpl()                -- no-op.
+/// - beginModule() collects compile-unit metadata.
+/// - prepareModuleOutput() adds the extension and ext-inst set to MAI.
+/// - emitNonSemanticDebugStrings() emits NSDI OpStrings in section 7.
+/// - emitNonSemanticGlobalDebugInfo() emits module-scope NSDI and sets
+///   GlobalNSDIEnabled.
+/// - beginFunctionImpl() prepares per-function DebugFunctionDefinition state.
+/// - SPIRVAsmPrinter notifies the handler when it emits MachineInstrs and the
+///   synthesized entry OpLabel.
+/// - endFunctionImpl() resets per-function state.
 class SPIRVNonSemanticDebugHandler : public DebugHandlerBase {
   struct CompileUnitInfo {
     const DICompileUnit *TheCU = nullptr;
@@ -101,6 +100,10 @@ class SPIRVNonSemanticDebugHandler : public DebugHandlerBase {
   // (only entries where emission succeeded).
   DenseMap<const DISubprogram *, MCRegister> DebugFunctionDeclarationRegs;
 
+  // DebugFunction result id per emitted definition DISubprogram (only entries
+  // where emission succeeded).
+  DenseMap<const DISubprogram *, MCRegister> DebugFunctionRegs;
+
   // Path \c OpString result id per \c DIScope (CU, \c DIFile, declaration
   // \c DISubprogram, …). Filled during \c emitNonSemanticDebugStrings() using
   // \c getDebugFullPath + \c emitOpStringIfNew; section 10 uses it for
@@ -148,6 +151,20 @@ class SPIRVNonSemanticDebugHandler : public DebugHandlerBase {
   // change.
   bool GlobalDIEmitted = false;
 
+  // True when emitNonSemanticGlobalDebugInfo() completed module-scope NSDI
+  // emission for this module.
+  bool GlobalNSDIEnabled = false;
+
+  SPIRV::ModuleAnalysisInfo *CurrentMAI = nullptr;
+
+  MCRegister CachedExtInstSetReg;
+
+  const MachineFunction *CurrentMF = nullptr;
+
+  const MachineInstr *LastFunctionOpVariable = nullptr;
+
+  bool DebugFunctionDefinitionEmitted = false;
+
 public:
   explicit SPIRVNonSemanticDebugHandler(AsmPrinter &AP);
 
@@ -177,7 +194,17 @@ class SPIRVNonSemanticDebugHandler : public DebugHandlerBase {
   /// SPIRVAsmPrinter::outputModuleSections() at section 10 in place of
   /// outputModuleSection(MB_NonSemanticGlobalDI). Requires
   /// emitNonSemanticDebugStrings() to have run first when NSDI strings apply.
-  void emitNonSemanticGlobalDebugInfo(SPIRV::ModuleAnalysisInfo &MAI);
+  /// \returns true when module-scope NSDI emission ran; false when skipped.
+  bool emitNonSemanticGlobalDebugInfo(SPIRV::ModuleAnalysisInfo &MAI);
+
+  /// Called after an MI has been emitted.
+  void notifyMachineInstructionEmitted(const MachineInstr *MI,
+                                       const MachineFunction &MF,
+                                       SPIRV::ModuleAnalysisInfo &MAI);
+
+  /// Called after the synthesized entry \c OpLabel has been emitted.
+  void notifyEntryLabelEmitted(const MachineFunction &MF,
+                               SPIRV::ModuleAnalysisInfo &MAI);
 
 protected:
   // All module-level output is driven by emitNonSemanticGlobalDebugInfo(),
@@ -191,21 +218,23 @@ class SPIRVNonSemanticDebugHandler : public DebugHandlerBase {
   // point and MMI remains null for this handler's entire lifetime. The
   // base-class beginInstruction/endInstruction dereference MMI to create temp
   // symbols for label tracking and would crash. Override them as no-ops.
-  // When per-function NSDI is implemented, use Asm->OutStreamer->getContext()
-  // for MCContext access rather than MMI->getContext().
+  // Future local NSDI that needs MCContext must use
+  // Asm->OutStreamer->getContext() rather than MMI->getContext().
   void beginInstruction(const MachineInstr *MI) override {}
   void endInstruction() override {}
 
-  // TODO: Emit DebugFunctionDefinition here once per-function NSDI emission is
-  // implemented. DebugHandlerBase::beginFunction() populates LScopes and
-  // DbgValues, which are needed for DebugLine emission. Do not override
-  // beginFunction() until that work is in place.
-  void beginFunctionImpl(const MachineFunction *MF) override {}
-  // TODO: Add per-function cleanup when DebugFunctionDefinition emission is in
-  // place.
-  void endFunctionImpl(const MachineFunction *MF) override {}
+  void beginFunctionImpl(const MachineFunction *MF) override;
+  void endFunctionImpl(const MachineFunction *MF) override;
 
 private:
+  void emitDebugFunctionDefinition(MCRegister DebugFunctionReg,
+                                   MCRegister OpFunctionReg,
+                                   SPIRV::ModuleAnalysisInfo &MAI);
+
+  void resetPerFunctionDebugState();
+  void preparePerFunctionDebug(const MachineFunction *MF);
+  void tryEmitDebugFunctionDefinition(SPIRV::ModuleAnalysisInfo &MAI);
+
   void emitMCInst(MCInst &Inst);
   MCRegister emitOpString(StringRef S, SPIRV::ModuleAnalysisInfo &MAI);
 
diff --git a/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition-after-opvariable.ll b/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition-after-opvariable.ll
new file mode 100644
index 0000000000000..ec3c93c5115b6
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition-after-opvariable.ll
@@ -0,0 +1,36 @@
+; RUN: llc --verify-machineinstrs -O0 -mtriple=spirv64-unknown-unknown --spirv-ext=+SPV_KHR_non_semantic_info %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc --verify-machineinstrs --spirv-ext=+SPV_KHR_non_semantic_info -O0 -mtriple=spirv64-unknown-unknown %s -o - -filetype=obj | spirv-val %}
+
+; DebugFunctionDefinition must follow function-local OpVariable instructions.
+
+; CHECK-DAG: [[EXT:%[0-9]+]] = OpExtInstImport "NonSemantic.Shader.DebugInfo.100"
+; CHECK-DAG: [[VOID:%[0-9]+]] = OpTypeVoid
+; CHECK-DAG: [[DF:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugFunction {{.*}}
+; CHECK: [[FOO:%[0-9]+]] = OpFunction
+; CHECK: OpVariable {{.*}} Function
+; CHECK-NEXT: OpVariable {{.*}} Function
+; CHECK-NEXT: OpExtInst [[VOID]] [[EXT]] DebugFunctionDefinition [[DF]] [[FOO]]
+
+target triple = "spirv64-unknown-unknown"
+
+define spir_func void @foo() !dbg !4 {
+entry:
+  %x = alloca i32, align 4
+  %y = alloca i32, align 4
+  store i32 0, ptr %x
+  store i32 1, ptr %y
+  ret void, !dbg !7
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C99, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: None)
+!1 = !DIFile(filename: "debug-function-definition-after-opvariable.c", directory: "/src")
+!2 = !{i32 7, !"Dwarf Version", i32 5}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+
+!4 = distinct !DISubprogram(name: "foo", scope: !1, file: !1, line: 1, type: !5, scopeLine: 1, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0)
+!5 = !DISubroutineType(types: !6)
+!6 = !{null}
+!7 = !DILocation(line: 2, column: 1, scope: !4)
diff --git a/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition.ll b/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition.ll
new file mode 100644
index 0000000000000..5cacfd5c14281
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition.ll
@@ -0,0 +1,40 @@
+; RUN: llc --verify-machineinstrs -O0 -mtriple=spirv64-unknown-unknown --spirv-ext=+SPV_KHR_non_semantic_info %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc --verify-machineinstrs --spirv-ext=+SPV_KHR_non_semantic_info -O0 -mtriple=spirv64-unknown-unknown %s -o - -filetype=obj | spirv-val %}
+
+; Exercise NonSemantic DebugFunctionDefinition for a defined function.
+
+; CHECK-DAG: [[EXT:%[0-9]+]] = OpExtInstImport "NonSemantic.Shader.DebugInfo.100"
+; CHECK-DAG: [[VOID:%[0-9]+]] = OpTypeVoid
+; CHECK-DAG: [[I32:%[0-9]+]] = OpTypeInt 32 0
+; CHECK-DAG: [[PATH:%[0-9]+]] = OpString "/src/debug-function-definition.c"
+; CHECK-DAG: [[NAME:%[0-9]+]] = OpString "add_one"
+; CHECK-DAG: [[DS:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugSource [[PATH]]
+; CHECK-DAG: [[CU:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugCompilationUnit {{.*}}
+; CHECK-DAG: [[TF:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugTypeFunction {{.*}}
+; CHECK-DAG: [[DF:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugFunction [[NAME]] [[TF]] [[DS]] {{.*}}
+; CHECK: [[ADD_ONE:%[0-9]+]] = OpFunction
+; CHECK: OpLabel
+; CHECK-NEXT: OpExtInst [[VOID]] [[EXT]] DebugFunctionDefinition [[DF]] [[ADD_ONE]]
+
+target triple = "spirv64-unknown-unknown"
+
+define spir_func i32 @add_one(i32 %value) !dbg !5 {
+entry:
+  %result = add i32 %value, 1
+  ret i32 %result, !dbg !8
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C99, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: None)
+!1 = !DIFile(filename: "debug-function-definition.c", directory: "/src")
+!2 = !{i32 7, !"Dwarf Version", i32 5}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+
+!4 = !DISubroutineType(types: !6)
+!6 = !{!7, !7}
+!7 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+
+!5 = distinct !DISubprogram(name: "add_one", linkageName: "add_one", scope: !1, file: !1, line: 1, type: !4, scopeLine: 1, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0)
+!8 = !DILocation(line: 3, column: 3, scope: !5)

>From 0878f72e9418ca11fbdf584472917383d9576afd Mon Sep 17 00:00:00 2001
From: Manuel Carrasco <Manuel.Carrasco at amd.com>
Date: Fri, 24 Jul 2026 11:51:06 -0500
Subject: [PATCH 07/16] Keep original comment.

---
 llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h
index b1793cc0db811..5cf5464d1926c 100644
--- a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h
+++ b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h
@@ -223,6 +223,9 @@ class SPIRVNonSemanticDebugHandler : public DebugHandlerBase {
   void beginInstruction(const MachineInstr *MI) override {}
   void endInstruction() override {}
 
+  // Override beginFunctionImpl(), not beginFunction():
+  // DebugHandlerBase::beginFunction() populates LScopes and DbgValues needed
+  // for future DebugLine emission.
   void beginFunctionImpl(const MachineFunction *MF) override;
   void endFunctionImpl(const MachineFunction *MF) override;
 

>From 322607ca8357eabca0d24e23195b591b2d23832e Mon Sep 17 00:00:00 2001
From: Manuel Carrasco <Manuel.Carrasco at amd.com>
Date: Tue, 28 Jul 2026 10:33:43 -0500
Subject: [PATCH 08/16] [SPIRV] Drive DebugFunctionDefinition placement via
 begin/endInstruction

Replace the SPIRV-specific notifyMachineInstructionEmitted() callback with
DebugHandlerBase::beginInstruction()/endInstruction().
Keep notifyEntryLabelEmitted() for the synthesized entry OpLabel, which
has no MachineInstr.
---
 llvm/lib/Target/SPIRV/SPIRVAsmPrinter.cpp     |  7 ++--
 .../SPIRV/SPIRVNonSemanticDebugHandler.cpp    | 36 +++++++++++--------
 .../SPIRV/SPIRVNonSemanticDebugHandler.h      | 20 +++++------
 3 files changed, 32 insertions(+), 31 deletions(-)

diff --git a/llvm/lib/Target/SPIRV/SPIRVAsmPrinter.cpp b/llvm/lib/Target/SPIRV/SPIRVAsmPrinter.cpp
index 4c9c0255397cc..7376e97b80901 100644
--- a/llvm/lib/Target/SPIRV/SPIRVAsmPrinter.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVAsmPrinter.cpp
@@ -323,11 +323,8 @@ void SPIRVAsmPrinter::emitInstruction(const MachineInstr *MI) {
                                         getSubtargetInfo().getFeatureBits());
 
   bool InstructionEmitted = !MAI->getSkipEmission(MI);
-  if (InstructionEmitted) {
+  if (InstructionEmitted)
     outputInstruction(MI);
-    if (NSDebugHandler && !isHidden() && MAI)
-      NSDebugHandler->notifyMachineInstructionEmitted(MI, *MF, *MAI);
-  }
 
   // Output OpLabel after OpFunction and OpFunctionParameter in the first MBB.
   const MachineInstr *NextMI = MI->getNextNode();
@@ -341,7 +338,7 @@ void SPIRVAsmPrinter::emitInstruction(const MachineInstr *MI) {
     assert(MI->getParent()->getNumber() == MF->front().getNumber() &&
            "OpFunction is not in the front MBB of MF");
     emitOpLabel(*MI->getParent());
-    if (NSDebugHandler && !isHidden() && MAI)
+    if (NSDebugHandler && !isHidden())
       NSDebugHandler->notifyEntryLabelEmitted(*MF, *MAI);
   }
 }
diff --git a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
index 23674755b2c17..b6f0bd98bf377 100644
--- a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
@@ -1163,35 +1163,43 @@ void SPIRVNonSemanticDebugHandler::endFunctionImpl(const MachineFunction *MF) {
   resetPerFunctionDebugState();
 }
 
-void SPIRVNonSemanticDebugHandler::notifyMachineInstructionEmitted(
-    const MachineInstr *MI, const MachineFunction &MF,
-    SPIRV::ModuleAnalysisInfo &MAI) {
-  if (!GlobalNSDIEnabled || DebugFunctionDefinitionEmitted)
+void SPIRVNonSemanticDebugHandler::beginInstruction(const MachineInstr *MI) {
+  assert(CurMI == nullptr && "CurMI must be null");
+  CurMI = MI;
+}
+
+void SPIRVNonSemanticDebugHandler::endInstruction() {
+  const MachineInstr *MI = CurMI;
+  CurMI = nullptr;
+
+  if (!MI || !GlobalNSDIEnabled || DebugFunctionDefinitionEmitted || !CurrentMF)
     return;
-  assert(CurrentMF == &MF &&
-         "notification does not match the current MachineFunction");
-  if (MI->getParent() != &MF.front())
+
+  if (MI != LastFunctionOpVariable)
     return;
 
   // If this is the last function-level OpVariable, emit the
   // DebugFunctionDefinition. Otherwise, we had already done it before right
-  // after the OpLabel.
-  if (MI == LastFunctionOpVariable)
-    tryEmitDebugFunctionDefinition(MAI);
+  // after the OpLabel (see notifyEntryLabelEmitted).
+  assert(CurrentMAI && "CurrentMAI must be set");
+  tryEmitDebugFunctionDefinition(*CurrentMAI);
 }
 
 void SPIRVNonSemanticDebugHandler::notifyEntryLabelEmitted(
     const MachineFunction &MF, SPIRV::ModuleAnalysisInfo &MAI) {
-  if (!GlobalNSDIEnabled || DebugFunctionDefinitionEmitted)
+  if (!GlobalNSDIEnabled || DebugFunctionDefinitionEmitted || !CurrentMF)
     return;
+
   assert(CurrentMF == &MF &&
          "notification does not match the current MachineFunction");
 
+  if (LastFunctionOpVariable)
+    return;
+
   // If there are no function-level OpVariables, emit the
   // DebugFunctionDefinition. Otherwise, DebugFunctionDefinition is emitted
-  // after the last OpVariable.
-  if (!LastFunctionOpVariable)
-    tryEmitDebugFunctionDefinition(MAI);
+  // after the last OpVariable (see endInstruction).
+  tryEmitDebugFunctionDefinition(*CurrentMAI);
 }
 
 bool SPIRVNonSemanticDebugHandler::emitNonSemanticGlobalDebugInfo(
diff --git a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h
index 5cf5464d1926c..bd4752e85f06c 100644
--- a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h
+++ b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h
@@ -46,8 +46,9 @@ class SPIRVSubtarget;
 /// - emitNonSemanticGlobalDebugInfo() emits module-scope NSDI and sets
 ///   GlobalNSDIEnabled.
 /// - beginFunctionImpl() prepares per-function DebugFunctionDefinition state.
-/// - SPIRVAsmPrinter notifies the handler when it emits MachineInstrs and the
-///   synthesized entry OpLabel.
+/// - endInstruction() emits DebugFunctionDefinition after the last function-
+///   level OpVariable; SPIRVAsmPrinter calls notifyEntryLabelEmitted() after
+///   the synthesized entry OpLabel when there are no OpVariables.
 /// - endFunctionImpl() resets per-function state.
 class SPIRVNonSemanticDebugHandler : public DebugHandlerBase {
   struct CompileUnitInfo {
@@ -197,11 +198,6 @@ class SPIRVNonSemanticDebugHandler : public DebugHandlerBase {
   /// \returns true when module-scope NSDI emission ran; false when skipped.
   bool emitNonSemanticGlobalDebugInfo(SPIRV::ModuleAnalysisInfo &MAI);
 
-  /// Called after an MI has been emitted.
-  void notifyMachineInstructionEmitted(const MachineInstr *MI,
-                                       const MachineFunction &MF,
-                                       SPIRV::ModuleAnalysisInfo &MAI);
-
   /// Called after the synthesized entry \c OpLabel has been emitted.
   void notifyEntryLabelEmitted(const MachineFunction &MF,
                                SPIRV::ModuleAnalysisInfo &MAI);
@@ -215,13 +211,13 @@ class SPIRVNonSemanticDebugHandler : public DebugHandlerBase {
   // DebugHandlerBase stores MMI as a pointer copy from Asm->MMI at construction
   // time (DebugHandlerBase.cpp: `MMI(Asm->MMI)`). The handler is constructed
   // before AsmPrinter::doInitialization() runs, so Asm->MMI is null at that
-  // point and MMI remains null for this handler's entire lifetime. The
-  // base-class beginInstruction/endInstruction dereference MMI to create temp
-  // symbols for label tracking and would crash. Override them as no-ops.
+  // point and MMI remains null for this handler's entire lifetime. Do not call
+  // the base-class beginInstruction/endInstruction — they dereference MMI to
+  // create temp symbols for label tracking and would crash.
   // Future local NSDI that needs MCContext must use
   // Asm->OutStreamer->getContext() rather than MMI->getContext().
-  void beginInstruction(const MachineInstr *MI) override {}
-  void endInstruction() override {}
+  void beginInstruction(const MachineInstr *MI) override;
+  void endInstruction() override;
 
   // Override beginFunctionImpl(), not beginFunction():
   // DebugHandlerBase::beginFunction() populates LScopes and DbgValues needed

>From fb8a7d5697e41720a41b5f11c86a73a89a911753 Mon Sep 17 00:00:00 2001
From: Manuel Carrasco <Manuel.Carrasco at amd.com>
Date: Tue, 28 Jul 2026 10:53:42 -0500
Subject: [PATCH 09/16] Fix path check.

---
 llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition.ll | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition.ll b/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition.ll
index 5cacfd5c14281..96007db44ea3c 100644
--- a/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition.ll
+++ b/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition.ll
@@ -6,7 +6,7 @@
 ; CHECK-DAG: [[EXT:%[0-9]+]] = OpExtInstImport "NonSemantic.Shader.DebugInfo.100"
 ; CHECK-DAG: [[VOID:%[0-9]+]] = OpTypeVoid
 ; CHECK-DAG: [[I32:%[0-9]+]] = OpTypeInt 32 0
-; CHECK-DAG: [[PATH:%[0-9]+]] = OpString "/src/debug-function-definition.c"
+; CHECK-DAG: [[PATH:%[0-9]+]] = OpString "{{[/\\]}}src{{[/\\]}}debug-function-definition.c"
 ; CHECK-DAG: [[NAME:%[0-9]+]] = OpString "add_one"
 ; CHECK-DAG: [[DS:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugSource [[PATH]]
 ; CHECK-DAG: [[CU:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugCompilationUnit {{.*}}

>From b94cdf1046c36557a13e4c00009d87afcfb231a1 Mon Sep 17 00:00:00 2001
From: Manuel Carrasco <Manuel.Carrasco at amd.com>
Date: Tue, 28 Jul 2026 10:59:54 -0500
Subject: [PATCH 10/16] Remove CachedExtInstSetReg.

---
 .../lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp | 11 ++---------
 llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h  |  5 +++--
 2 files changed, 5 insertions(+), 11 deletions(-)

diff --git a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
index b6f0bd98bf377..9fcb75d6ecc0d 100644
--- a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
@@ -284,7 +284,6 @@ void SPIRVNonSemanticDebugHandler::beginModule(Module *M) {
   GlobalDIEmitted = false;
   GlobalNSDIEnabled = false;
   CurrentMAI = nullptr;
-  CachedExtInstSetReg = MCRegister();
 #ifndef NDEBUG
   NonSemanticOpStringsSectionEmitted = false;
 #endif
@@ -371,8 +370,6 @@ void SPIRVNonSemanticDebugHandler::prepareModuleOutput(
   // Add the NonSemantic.Shader.DebugInfo.100 entry to ExtInstSetMap so that
   // outputOpExtInstImports() emits the OpExtInstImport instruction. Allocate a
   // fresh result ID for it now; the same ID is used in emitExtInst() operands.
-  constexpr unsigned NSSet = static_cast<unsigned>(
-      SPIRV::InstructionSet::NonSemantic_Shader_DebugInfo_100);
   if (!MAI.ExtInstSetMap.count(NSSet))
     MAI.ExtInstSetMap[NSSet] = MAI.getNextIDRegister();
 }
@@ -1027,8 +1024,6 @@ void SPIRVNonSemanticDebugHandler::emitNonSemanticDebugStrings(
   // Check that prepareModuleOutput() registered the extended instruction set.
   // If the subtarget does not support the extension, neither strings nor ext
   // insts are emitted.
-  constexpr unsigned NSSet = static_cast<unsigned>(
-      SPIRV::InstructionSet::NonSemantic_Shader_DebugInfo_100);
   if (!MAI.getExtInstSetReg(NSSet).isValid())
     return;
 
@@ -1098,8 +1093,9 @@ void SPIRVNonSemanticDebugHandler::emitDebugFunctionDefinition(
   assert(DebugFunctionReg.isValid() && OpFunctionReg.isValid() &&
          "DebugFunctionDefinition operands must be valid");
   MCRegister VoidTypeReg = getOrEmitOpTypeVoidReg(MAI);
+  MCRegister ExtInstSetReg = MAI.getExtInstSetReg(NSSet);
   emitExtInst(SPIRV::NonSemanticExtInst::DebugFunctionDefinition, VoidTypeReg,
-              CachedExtInstSetReg, {DebugFunctionReg, OpFunctionReg}, MAI);
+              ExtInstSetReg, {DebugFunctionReg, OpFunctionReg}, MAI);
 }
 
 void SPIRVNonSemanticDebugHandler::resetPerFunctionDebugState() {
@@ -1215,8 +1211,6 @@ bool SPIRVNonSemanticDebugHandler::emitNonSemanticGlobalDebugInfo(
   }
 
   // Retrieve the ext inst set register allocated by prepareModuleOutput().
-  constexpr unsigned NSSet = static_cast<unsigned>(
-      SPIRV::InstructionSet::NonSemantic_Shader_DebugInfo_100);
   MCRegister ExtInstSetReg = MAI.getExtInstSetReg(NSSet);
   if (!ExtInstSetReg.isValid()) {
     GlobalNSDIEnabled = false;
@@ -1230,7 +1224,6 @@ bool SPIRVNonSemanticDebugHandler::emitNonSemanticGlobalDebugInfo(
 #endif
 
   CurrentMAI = &MAI;
-  CachedExtInstSetReg = ExtInstSetReg;
 
   MCRegister VoidTypeReg = getOrEmitOpTypeVoidReg(MAI);
   MCRegister I32TypeReg = getOrEmitOpTypeInt32Reg(MAI);
diff --git a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h
index bd4752e85f06c..e8b5bab3b2d15 100644
--- a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h
+++ b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h
@@ -51,6 +51,9 @@ class SPIRVSubtarget;
 ///   the synthesized entry OpLabel when there are no OpVariables.
 /// - endFunctionImpl() resets per-function state.
 class SPIRVNonSemanticDebugHandler : public DebugHandlerBase {
+  static constexpr unsigned NSSet = static_cast<unsigned>(
+      SPIRV::InstructionSet::NonSemantic_Shader_DebugInfo_100);
+
   struct CompileUnitInfo {
     const DICompileUnit *TheCU = nullptr;
     SmallString<128> FilePath;
@@ -158,8 +161,6 @@ class SPIRVNonSemanticDebugHandler : public DebugHandlerBase {
 
   SPIRV::ModuleAnalysisInfo *CurrentMAI = nullptr;
 
-  MCRegister CachedExtInstSetReg;
-
   const MachineFunction *CurrentMF = nullptr;
 
   const MachineInstr *LastFunctionOpVariable = nullptr;

>From becba347375056f537e4a4b5a15019316a3e8b4d Mon Sep 17 00:00:00 2001
From: Manuel Carrasco <Manuel.Carrasco at amd.com>
Date: Tue, 28 Jul 2026 11:05:46 -0500
Subject: [PATCH 11/16] Change signature.

---
 llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp | 9 ++++-----
 llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h   | 4 ++--
 2 files changed, 6 insertions(+), 7 deletions(-)

diff --git a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
index 9fcb75d6ecc0d..d8bffd54500a9 100644
--- a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
@@ -1198,23 +1198,23 @@ void SPIRVNonSemanticDebugHandler::notifyEntryLabelEmitted(
   tryEmitDebugFunctionDefinition(*CurrentMAI);
 }
 
-bool SPIRVNonSemanticDebugHandler::emitNonSemanticGlobalDebugInfo(
+void SPIRVNonSemanticDebugHandler::emitNonSemanticGlobalDebugInfo(
     SPIRV::ModuleAnalysisInfo &MAI) {
   if (GlobalDIEmitted)
-    return GlobalNSDIEnabled;
+    return;
 
   GlobalDIEmitted = true;
 
   if (CompileUnits.empty()) {
     GlobalNSDIEnabled = false;
-    return false;
+    return;
   }
 
   // Retrieve the ext inst set register allocated by prepareModuleOutput().
   MCRegister ExtInstSetReg = MAI.getExtInstSetReg(NSSet);
   if (!ExtInstSetReg.isValid()) {
     GlobalNSDIEnabled = false;
-    return false;
+    return;
   }
 
 #ifndef NDEBUG
@@ -1390,7 +1390,6 @@ bool SPIRVNonSemanticDebugHandler::emitNonSemanticGlobalDebugInfo(
                             MAI);
 
   GlobalNSDIEnabled = true;
-  return true;
 }
 
 SmallString<128>
diff --git a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h
index e8b5bab3b2d15..b89381a2e07a6 100644
--- a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h
+++ b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h
@@ -196,8 +196,8 @@ class SPIRVNonSemanticDebugHandler : public DebugHandlerBase {
   /// SPIRVAsmPrinter::outputModuleSections() at section 10 in place of
   /// outputModuleSection(MB_NonSemanticGlobalDI). Requires
   /// emitNonSemanticDebugStrings() to have run first when NSDI strings apply.
-  /// \returns true when module-scope NSDI emission ran; false when skipped.
-  bool emitNonSemanticGlobalDebugInfo(SPIRV::ModuleAnalysisInfo &MAI);
+  /// Sets \c GlobalNSDIEnabled when module-scope NSDI emission completes.
+  void emitNonSemanticGlobalDebugInfo(SPIRV::ModuleAnalysisInfo &MAI);
 
   /// Called after the synthesized entry \c OpLabel has been emitted.
   void notifyEntryLabelEmitted(const MachineFunction &MF,

>From f2541dea6002c4fd1c13bc26d87b46412888f818 Mon Sep 17 00:00:00 2001
From: Manuel Carrasco <Manuel.Carrasco at amd.com>
Date: Tue, 28 Jul 2026 11:17:02 -0500
Subject: [PATCH 12/16] Improve test.

---
 .../debug-info/debug-function-definition.ll   | 23 +++++++++++++++----
 1 file changed, 19 insertions(+), 4 deletions(-)

diff --git a/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition.ll b/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition.ll
index 96007db44ea3c..57c246edff8f3 100644
--- a/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition.ll
+++ b/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition.ll
@@ -1,20 +1,26 @@
 ; RUN: llc --verify-machineinstrs -O0 -mtriple=spirv64-unknown-unknown --spirv-ext=+SPV_KHR_non_semantic_info %s -o - | FileCheck %s
 ; RUN: %if spirv-tools %{ llc --verify-machineinstrs --spirv-ext=+SPV_KHR_non_semantic_info -O0 -mtriple=spirv64-unknown-unknown %s -o - -filetype=obj | spirv-val %}
 
-; Exercise NonSemantic DebugFunctionDefinition for a defined function.
+; Exercise NonSemantic DebugFunctionDefinition for defined functions, including
+; per-function emitter state across multiple definitions in one module.
 
 ; CHECK-DAG: [[EXT:%[0-9]+]] = OpExtInstImport "NonSemantic.Shader.DebugInfo.100"
 ; CHECK-DAG: [[VOID:%[0-9]+]] = OpTypeVoid
 ; CHECK-DAG: [[I32:%[0-9]+]] = OpTypeInt 32 0
 ; CHECK-DAG: [[PATH:%[0-9]+]] = OpString "{{[/\\]}}src{{[/\\]}}debug-function-definition.c"
-; CHECK-DAG: [[NAME:%[0-9]+]] = OpString "add_one"
+; CHECK-DAG: [[NAME1:%[0-9]+]] = OpString "add_one"
+; CHECK-DAG: [[NAME2:%[0-9]+]] = OpString "add_two"
 ; CHECK-DAG: [[DS:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugSource [[PATH]]
 ; CHECK-DAG: [[CU:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugCompilationUnit {{.*}}
 ; CHECK-DAG: [[TF:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugTypeFunction {{.*}}
-; CHECK-DAG: [[DF:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugFunction [[NAME]] [[TF]] [[DS]] {{.*}}
+; CHECK-DAG: [[DF1:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugFunction [[NAME1]] [[TF]] [[DS]] {{.*}}
+; CHECK-DAG: [[DF2:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugFunction [[NAME2]] [[TF]] [[DS]] {{.*}}
 ; CHECK: [[ADD_ONE:%[0-9]+]] = OpFunction
 ; CHECK: OpLabel
-; CHECK-NEXT: OpExtInst [[VOID]] [[EXT]] DebugFunctionDefinition [[DF]] [[ADD_ONE]]
+; CHECK-NEXT: OpExtInst [[VOID]] [[EXT]] DebugFunctionDefinition [[DF1]] [[ADD_ONE]]
+; CHECK: [[ADD_TWO:%[0-9]+]] = OpFunction
+; CHECK: OpLabel
+; CHECK-NEXT: OpExtInst [[VOID]] [[EXT]] DebugFunctionDefinition [[DF2]] [[ADD_TWO]]
 
 target triple = "spirv64-unknown-unknown"
 
@@ -24,6 +30,12 @@ entry:
   ret i32 %result, !dbg !8
 }
 
+define spir_func i32 @add_two(i32 %value) !dbg !9 {
+entry:
+  %result = add i32 %value, 2
+  ret i32 %result, !dbg !10
+}
+
 !llvm.dbg.cu = !{!0}
 !llvm.module.flags = !{!2, !3}
 
@@ -38,3 +50,6 @@ entry:
 
 !5 = distinct !DISubprogram(name: "add_one", linkageName: "add_one", scope: !1, file: !1, line: 1, type: !4, scopeLine: 1, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0)
 !8 = !DILocation(line: 3, column: 3, scope: !5)
+
+!9 = distinct !DISubprogram(name: "add_two", linkageName: "add_two", scope: !1, file: !1, line: 5, type: !4, scopeLine: 5, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0)
+!10 = !DILocation(line: 7, column: 3, scope: !9)

>From f07070a0909e6ba00dcdf519b442c7ccf33e69eb Mon Sep 17 00:00:00 2001
From: Manuel Carrasco <Manuel.Carrasco at amd.com>
Date: Tue, 28 Jul 2026 11:45:49 -0500
Subject: [PATCH 13/16] Fix rebase.

---
 llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
index d8bffd54500a9..120f03c279247 100644
--- a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
@@ -1068,12 +1068,6 @@ void SPIRVNonSemanticDebugHandler::emitNonSemanticDebugStrings(
     emitAndCacheScopePathOpStringReg(TD->getFile(), MAI);
   }
 
-  for (const DISubprogram *SP : SubprogramDefinitions) {
-    emitOpStringIfNew(SP->getName(), MAI);
-    emitOpStringIfNew(SP->getLinkageName(), MAI);
-    ScopeToPathOpStringReg[SP] = emitOpStringIfNew(getDebugFullPath(SP), MAI);
-  }
-
   for (const auto &[GV, _] : GlobalVariableDebugInfoMap) {
     emitOpStringIfNew(GV->getName(), MAI);
     emitOpStringIfNew(GV->getLinkageName(), MAI);

>From 0dad4a04c6378e42a4b1641eaf20d571dd6462cc Mon Sep 17 00:00:00 2001
From: Manuel Carrasco <Manuel.Carrasco at amd.com>
Date: Thu, 30 Jul 2026 09:22:35 -0500
Subject: [PATCH 14/16] Remove unused parameter.

---
 llvm/lib/Target/SPIRV/SPIRVAsmPrinter.cpp              | 2 +-
 llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp | 2 +-
 llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h   | 3 +--
 3 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/llvm/lib/Target/SPIRV/SPIRVAsmPrinter.cpp b/llvm/lib/Target/SPIRV/SPIRVAsmPrinter.cpp
index 7376e97b80901..040a6858b9009 100644
--- a/llvm/lib/Target/SPIRV/SPIRVAsmPrinter.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVAsmPrinter.cpp
@@ -339,7 +339,7 @@ void SPIRVAsmPrinter::emitInstruction(const MachineInstr *MI) {
            "OpFunction is not in the front MBB of MF");
     emitOpLabel(*MI->getParent());
     if (NSDebugHandler && !isHidden())
-      NSDebugHandler->notifyEntryLabelEmitted(*MF, *MAI);
+      NSDebugHandler->notifyEntryLabelEmitted(*MF);
   }
 }
 
diff --git a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
index 120f03c279247..96d874eb017bb 100644
--- a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
@@ -1176,7 +1176,7 @@ void SPIRVNonSemanticDebugHandler::endInstruction() {
 }
 
 void SPIRVNonSemanticDebugHandler::notifyEntryLabelEmitted(
-    const MachineFunction &MF, SPIRV::ModuleAnalysisInfo &MAI) {
+    const MachineFunction &MF) {
   if (!GlobalNSDIEnabled || DebugFunctionDefinitionEmitted || !CurrentMF)
     return;
 
diff --git a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h
index b89381a2e07a6..4fce1b73004e1 100644
--- a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h
+++ b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.h
@@ -200,8 +200,7 @@ class SPIRVNonSemanticDebugHandler : public DebugHandlerBase {
   void emitNonSemanticGlobalDebugInfo(SPIRV::ModuleAnalysisInfo &MAI);
 
   /// Called after the synthesized entry \c OpLabel has been emitted.
-  void notifyEntryLabelEmitted(const MachineFunction &MF,
-                               SPIRV::ModuleAnalysisInfo &MAI);
+  void notifyEntryLabelEmitted(const MachineFunction &MF);
 
 protected:
   // All module-level output is driven by emitNonSemanticGlobalDebugInfo(),

>From ee20b2c4f564181423b71a2ea88fb6330c16a345 Mon Sep 17 00:00:00 2001
From: Manuel Carrasco <Manuel.Carrasco at amd.com>
Date: Mon, 3 Aug 2026 05:55:37 -0500
Subject: [PATCH 15/16] Improve tests.

---
 ...-function-definition-call-before-alloca.ll |  88 +++++++++++++
 .../debug-function-definition-calls.ll        | 124 ++++++++++++++++++
 ...debug-function-definition-external-call.ll |  56 ++++++++
 3 files changed, 268 insertions(+)
 create mode 100644 llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition-call-before-alloca.ll
 create mode 100644 llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition-calls.ll
 create mode 100644 llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition-external-call.ll

diff --git a/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition-call-before-alloca.ll b/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition-call-before-alloca.ll
new file mode 100644
index 0000000000000..19d9c1b09db2d
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition-call-before-alloca.ll
@@ -0,0 +1,88 @@
+; RUN: llc --verify-machineinstrs -O0 -mtriple=spirv64-unknown-unknown --spirv-ext=+SPV_KHR_non_semantic_info %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc --verify-machineinstrs --spirv-ext=+SPV_KHR_non_semantic_info -O0 -mtriple=spirv64-unknown-unknown %s -o - -filetype=obj | spirv-val %}
+
+; External call appears before the alloca in LLVM IR source order. The local
+; OpVariable is still inserted in the function preamble; DebugFunctionDefinition
+; must follow it, not the entry OpLabel.
+
+; CHECK-DAG: [[EXT:%[0-9]+]] = OpExtInstImport "NonSemantic.Shader.DebugInfo.100"
+; CHECK-DAG: [[VOID:%[0-9]+]] = OpTypeVoid
+; CHECK-DAG: [[NAME:%[0-9]+]] = OpString "caller"
+; CHECK-DAG: [[NAME_NOARGS:%[0-9]+]] = OpString "caller_no_args"
+; CHECK-DAG: [[DF:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugFunction [[NAME]]
+; CHECK-DAG: [[DF_NOARGS:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugFunction [[NAME_NOARGS]]
+; CHECK-DAG: OpName [[EXT_HELPER:%[0-9]+]] "external_helper"
+; CHECK-DAG: OpDecorate [[EXT_HELPER]] LinkageAttributes "external_helper" Import
+; CHECK-DAG: OpDecorate [[CALLER:%[0-9]+]] LinkageAttributes "caller" Export
+; CHECK-DAG: OpName [[EXT_HELPER_NOARGS:%[0-9]+]] "external_helper_no_args"
+; CHECK-DAG: OpDecorate [[EXT_HELPER_NOARGS]] LinkageAttributes "external_helper_no_args" Import
+; CHECK-DAG: OpDecorate [[CALLER_NOARGS:%[0-9]+]] LinkageAttributes "caller_no_args" Export
+
+; external_helper: hoisted declaration in the declarations section.
+; CHECK: [[EXT_HELPER]] = OpFunction %{{.*}}
+; CHECK-NEXT: OpFunctionParameter
+; CHECK-NEXT: OpFunctionEnd
+
+; CHECK: [[EXT_HELPER_NOARGS]] = OpFunction %{{.*}}
+; CHECK-NEXT: OpFunctionEnd
+
+; CHECK: [[CALLER]] = OpFunction %{{.*}} ; -- Begin function caller
+; CHECK-NEXT: OpFunctionParameter
+; CHECK-NEXT: OpLabel
+; CHECK-NEXT: OpVariable {{.*}} Function
+; CHECK-NEXT: OpExtInst [[VOID]] [[EXT]] DebugFunctionDefinition [[DF]] [[CALLER]]
+; CHECK-NEXT: OpFunctionCall
+; CHECK-NEXT: OpStore
+; CHECK-NEXT: OpReturnValue
+; CHECK-NEXT: OpFunctionEnd
+
+; caller_no_args: same placement rules, but no function parameters.
+; CHECK: [[CALLER_NOARGS]] = OpFunction %{{.*}} ; -- Begin function caller_no_args
+; CHECK-NEXT: OpLabel
+; CHECK-NEXT: OpVariable {{.*}} Function
+; CHECK-NEXT: OpExtInst [[VOID]] [[EXT]] DebugFunctionDefinition [[DF_NOARGS]] [[CALLER_NOARGS]]
+; CHECK-NEXT: OpFunctionCall
+; CHECK-NEXT: OpStore
+; CHECK-NEXT: OpReturn
+; CHECK-NEXT: OpFunctionEnd
+
+target triple = "spirv64-unknown-unknown"
+
+declare spir_func i32 @external_helper(i32)
+declare spir_func i32 @external_helper_no_args()
+
+define spir_func i32 @caller(i32 %x) !dbg !5 {
+entry:
+  %r = call i32 @external_helper(i32 %x)
+  %a = alloca i32, align 4
+  store i32 %r, ptr %a
+  ret i32 %r, !dbg !8
+}
+
+define spir_func void @caller_no_args() !dbg !9 {
+entry:
+  %r = call i32 @external_helper_no_args()
+  %a = alloca i32, align 4
+  store i32 %r, ptr %a
+  ret void, !dbg !13
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C99, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: None)
+!1 = !DIFile(filename: "debug-function-definition-call-before-alloca.c", directory: "/src")
+!2 = !{i32 7, !"Dwarf Version", i32 5}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+
+!4 = !DISubroutineType(types: !6)
+!6 = !{!7, !7}
+!7 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+
+!5 = distinct !DISubprogram(name: "caller", linkageName: "caller", scope: !1, file: !1, line: 1, type: !4, scopeLine: 1, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0)
+!8 = !DILocation(line: 6, column: 3, scope: !5)
+
+!9 = distinct !DISubprogram(name: "caller_no_args", linkageName: "caller_no_args", scope: !1, file: !1, line: 10, type: !10, scopeLine: 10, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0)
+!10 = !DISubroutineType(types: !11)
+!11 = !{null}
+!13 = !DILocation(line: 15, column: 3, scope: !9)
diff --git a/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition-calls.ll b/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition-calls.ll
new file mode 100644
index 0000000000000..19189b8d5bca7
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition-calls.ll
@@ -0,0 +1,124 @@
+; RUN: llc --verify-machineinstrs -O0 -mtriple=spirv64-unknown-unknown --spirv-ext=+SPV_KHR_non_semantic_info %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc --verify-machineinstrs --spirv-ext=+SPV_KHR_non_semantic_info -O0 -mtriple=spirv64-unknown-unknown %s -o - -filetype=obj | spirv-val %}
+
+; Exercise DebugFunctionDefinition placement across multiple defined functions
+; that call each other, with and without function-local OpVariable instructions.
+; Each function's definition must reference its own OpFunction id, placed after
+; the entry OpLabel (no locals) or after the last function-local OpVariable.
+
+; CHECK-DAG: [[EXT:%[0-9]+]] = OpExtInstImport "NonSemantic.Shader.DebugInfo.100"
+; CHECK-DAG: [[VOID:%[0-9]+]] = OpTypeVoid
+; CHECK-DAG: [[NAME_LEAF:%[0-9]+]] = OpString "leaf_no_vars"
+; CHECK-DAG: [[NAME_WITH:%[0-9]+]] = OpString "helper_with_vars"
+; CHECK-DAG: [[NAME_CALLER:%[0-9]+]] = OpString "caller_no_vars"
+; CHECK-DAG: [[NAME_ORCH:%[0-9]+]] = OpString "orchestrator"
+; CHECK-DAG: [[DF_LEAF:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugFunction [[NAME_LEAF]]
+; CHECK-DAG: [[DF_WITH:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugFunction [[NAME_WITH]]
+; CHECK-DAG: [[DF_CALLER:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugFunction [[NAME_CALLER]]
+; CHECK-DAG: [[DF_ORCH:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugFunction [[NAME_ORCH]]
+; CHECK-DAG: OpDecorate [[LEAF:%[0-9]+]] LinkageAttributes "leaf_no_vars" Export
+; CHECK-DAG: OpDecorate [[WITH:%[0-9]+]] LinkageAttributes "helper_with_vars" Export
+; CHECK-DAG: OpDecorate [[CALLER:%[0-9]+]] LinkageAttributes "caller_no_vars" Export
+; CHECK-DAG: OpDecorate [[ORCH:%[0-9]+]] LinkageAttributes "orchestrator" Export
+
+; leaf_no_vars: no local variables -> DebugFunctionDefinition after OpLabel.
+; CHECK: [[LEAF]] = OpFunction %{{.*}} ; -- Begin function leaf_no_vars
+; CHECK-NEXT: OpFunctionParameter
+; CHECK-NEXT: OpLabel
+; CHECK-NEXT: OpExtInst [[VOID]] [[EXT]] DebugFunctionDefinition [[DF_LEAF]] [[LEAF]]
+; CHECK-NEXT: OpReturnValue
+; CHECK-NEXT: OpFunctionEnd
+
+; helper_with_vars: two local variables -> DebugFunctionDefinition after them.
+; CHECK: [[WITH]] = OpFunction %{{.*}} ; -- Begin function helper_with_vars
+; CHECK-NEXT: OpFunctionParameter
+; CHECK-NEXT: OpLabel
+; CHECK-NEXT: OpVariable {{.*}} Function
+; CHECK-NEXT: OpVariable {{.*}} Function
+; CHECK-NEXT: OpExtInst [[VOID]] [[EXT]] DebugFunctionDefinition [[DF_WITH]] [[WITH]]
+; CHECK: OpReturnValue
+; CHECK-NEXT: OpFunctionEnd
+
+; caller_no_vars: calls both helpers, no locals -> after OpLabel.
+; CHECK: [[CALLER]] = OpFunction %{{.*}} ; -- Begin function caller_no_vars
+; CHECK-NEXT: OpFunctionParameter
+; CHECK-NEXT: OpLabel
+; CHECK-NEXT: OpExtInst [[VOID]] [[EXT]] DebugFunctionDefinition [[DF_CALLER]] [[CALLER]]
+; CHECK-NEXT: OpFunctionCall
+; CHECK-NEXT: OpFunctionCall
+; CHECK-NEXT: OpReturnValue
+; CHECK-NEXT: OpFunctionEnd
+
+; orchestrator: no args, no locals, only calls -> after OpLabel.
+; CHECK: [[ORCH]] = OpFunction %{{.*}} ; -- Begin function orchestrator
+; CHECK-NEXT: OpLabel
+; CHECK-NEXT: OpExtInst [[VOID]] [[EXT]] DebugFunctionDefinition [[DF_ORCH]] [[ORCH]]
+; CHECK-NEXT: OpFunctionCall
+; CHECK-NEXT: OpFunctionCall
+; CHECK-NEXT: OpReturn
+; CHECK-NEXT: OpFunctionEnd
+
+target triple = "spirv64-unknown-unknown"
+
+define spir_func i32 @leaf_no_vars(i32 %value) !dbg !5 {
+entry:
+  ret i32 %value, !dbg !8
+}
+
+define spir_func i32 @helper_with_vars(i32 %value) !dbg !9 {
+entry:
+  %x = alloca i32, align 4
+  %y = alloca i32, align 4
+  store i32 %value, ptr %x
+  store i32 0, ptr %y
+  %tmp = call spir_func i32 @leaf_no_vars(i32 %value), !dbg !12
+  %sum = add i32 %tmp, 1
+  ret i32 %sum, !dbg !13
+}
+
+define spir_func i32 @caller_no_vars(i32 %value) !dbg !14 {
+entry:
+  %a = call spir_func i32 @helper_with_vars(i32 %value), !dbg !17
+  %b = call spir_func i32 @leaf_no_vars(i32 %a), !dbg !18
+  ret i32 %b, !dbg !19
+}
+
+define spir_func void @orchestrator() !dbg !20 {
+entry:
+  %unused1 = call spir_func i32 @leaf_no_vars(i32 0), !dbg !23
+  %unused2 = call spir_func i32 @helper_with_vars(i32 1), !dbg !24
+  ret void, !dbg !25
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C99, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: None)
+!1 = !DIFile(filename: "debug-function-definition-calls.c", directory: "/src")
+!2 = !{i32 7, !"Dwarf Version", i32 5}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+
+!4 = !DISubroutineType(types: !6)
+!6 = !{!7, !7}
+!7 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+
+!5 = distinct !DISubprogram(name: "leaf_no_vars", linkageName: "leaf_no_vars", scope: !1, file: !1, line: 1, type: !4, scopeLine: 1, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0)
+!8 = !DILocation(line: 3, column: 3, scope: !5)
+
+!9 = distinct !DISubprogram(name: "helper_with_vars", linkageName: "helper_with_vars", scope: !1, file: !1, line: 5, type: !4, scopeLine: 5, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0)
+!10 = !DILocalVariable(name: "x", scope: !9, file: !1, line: 6, type: !7)
+!11 = !DILocalVariable(name: "y", scope: !9, file: !1, line: 7, type: !7)
+!12 = !DILocation(line: 10, column: 10, scope: !9)
+!13 = !DILocation(line: 12, column: 3, scope: !9)
+
+!14 = distinct !DISubprogram(name: "caller_no_vars", linkageName: "caller_no_vars", scope: !1, file: !1, line: 14, type: !4, scopeLine: 14, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0)
+!17 = !DILocation(line: 16, column: 8, scope: !14)
+!18 = !DILocation(line: 17, column: 8, scope: !14)
+!19 = !DILocation(line: 18, column: 3, scope: !14)
+
+!20 = distinct !DISubprogram(name: "orchestrator", linkageName: "orchestrator", scope: !1, file: !1, line: 20, type: !21, scopeLine: 20, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0)
+!21 = !DISubroutineType(types: !22)
+!22 = !{null}
+!23 = !DILocation(line: 22, column: 13, scope: !20)
+!24 = !DILocation(line: 23, column: 13, scope: !20)
+!25 = !DILocation(line: 24, column: 3, scope: !20)
diff --git a/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition-external-call.ll b/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition-external-call.ll
new file mode 100644
index 0000000000000..da5deeb43ac1f
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/debug-info/debug-function-definition-external-call.ll
@@ -0,0 +1,56 @@
+; RUN: llc --verify-machineinstrs -O0 -mtriple=spirv64-unknown-unknown --spirv-ext=+SPV_KHR_non_semantic_info %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc --verify-machineinstrs --spirv-ext=+SPV_KHR_non_semantic_info -O0 -mtriple=spirv64-unknown-unknown %s -o - -filetype=obj | spirv-val %}
+
+; Forward-declared external callee plus function-local variables: the hoisted
+; OpFunction declaration must not cause DebugFunctionDefinition to be emitted
+; before the last function-local OpVariable.
+
+; CHECK-DAG: [[EXT:%[0-9]+]] = OpExtInstImport "NonSemantic.Shader.DebugInfo.100"
+; CHECK-DAG: [[VOID:%[0-9]+]] = OpTypeVoid
+; CHECK-DAG: [[NAME:%[0-9]+]] = OpString "caller_with_vars"
+; CHECK-DAG: [[DF:%[0-9]+]] = OpExtInst [[VOID]] [[EXT]] DebugFunction [[NAME]]
+; CHECK-DAG: OpName [[EXT_HELPER:%[0-9]+]] "external_helper"
+; CHECK-DAG: OpDecorate [[EXT_HELPER]] LinkageAttributes "external_helper" Import
+; CHECK-DAG: OpDecorate [[CALLER:%[0-9]+]] LinkageAttributes "caller_with_vars" Export
+
+; external_helper: hoisted declaration in the declarations section.
+; CHECK: [[EXT_HELPER]] = OpFunction %{{.*}}
+; CHECK-NEXT: OpFunctionParameter
+; CHECK-NEXT: OpFunctionEnd
+
+; CHECK: [[CALLER]] = OpFunction %{{.*}} ; -- Begin function caller_with_vars
+; CHECK-NEXT: OpFunctionParameter
+; CHECK-NEXT: OpLabel
+; CHECK-NEXT: OpVariable {{.*}} Function
+; CHECK-NEXT: OpExtInst [[VOID]] [[EXT]] DebugFunctionDefinition [[DF]] [[CALLER]]
+; CHECK-NEXT: OpStore
+; CHECK-NEXT: OpFunctionCall
+; CHECK-NEXT: OpReturnValue
+; CHECK-NEXT: OpFunctionEnd
+
+target triple = "spirv64-unknown-unknown"
+
+declare spir_func i32 @external_helper(i32)
+
+define spir_func i32 @caller_with_vars(i32 %x) !dbg !5 {
+entry:
+  %a = alloca i32, align 4
+  store i32 %x, ptr %a
+  %r = call i32 @external_helper(i32 %x)
+  ret i32 %r, !dbg !8
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C99, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: None)
+!1 = !DIFile(filename: "debug-function-definition-external-call.c", directory: "/src")
+!2 = !{i32 7, !"Dwarf Version", i32 5}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+
+!4 = !DISubroutineType(types: !6)
+!6 = !{!7, !7}
+!7 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+
+!5 = distinct !DISubprogram(name: "caller_with_vars", linkageName: "caller_with_vars", scope: !1, file: !1, line: 1, type: !4, scopeLine: 1, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0)
+!8 = !DILocation(line: 5, column: 3, scope: !5)

>From cdad472b1c2d64a4a9eb1a230149bbeeb9665f1e Mon Sep 17 00:00:00 2001
From: Manuel Carrasco <Manuel.Carrasco at amd.com>
Date: Mon, 3 Aug 2026 05:56:00 -0500
Subject: [PATCH 16/16] Rename function.

---
 llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
index 96d874eb017bb..f921df3912d38 100644
--- a/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVNonSemanticDebugHandler.cpp
@@ -196,8 +196,8 @@ static uint32_t mapCompositeTypeTag(unsigned Tag) {
 }
 
 static const MachineInstr *
-findLastEmittedFunctionOpVariable(const MachineFunction &MF,
-                                  SPIRV::ModuleAnalysisInfo &MAI) {
+findLastFunctionOpVariableDeclaration(const MachineFunction &MF,
+                                      SPIRV::ModuleAnalysisInfo &MAI) {
 
   // We iterate over the instructions to find the last OpVariable instruction if
   // any. The following SPIRV rule is used to terminate the traversal earlier:
@@ -1117,7 +1117,8 @@ void SPIRVNonSemanticDebugHandler::preparePerFunctionDebug(
 
   // DebugFunctionDefinition is emitted after the last function-level
   // OpVariable. If there are none, it is emitted after the entry OpLabel.
-  LastFunctionOpVariable = findLastEmittedFunctionOpVariable(*MF, *CurrentMAI);
+  LastFunctionOpVariable =
+      findLastFunctionOpVariableDeclaration(*MF, *CurrentMAI);
 }
 
 void SPIRVNonSemanticDebugHandler::tryEmitDebugFunctionDefinition(



More information about the llvm-commits mailing list