Skip to content

Conversation

@adam-yang
Copy link
Contributor

@adam-yang adam-yang commented Sep 19, 2025

--report=debugger is a new report mode for llvm-debuginfo-analyzer that prints out the debug info logical view in a way that matches more closely to a debugger. By default, --report=debugger prints out every statement line and its source location and call stack. Take this source for example:

/*01*/ float bar(float b) {
/*02*/   float ret = sin(b);
/*03*/   return ret;
/*04*/ }
/*05*/ 
/*06*/ float foo(float a) {
/*07*/   float my_var = sin(a);
/*08*/   float my_var2 = my_var * 3;
/*09*/   float my_var3 = bar(my_var2);
/*10*/   return my_var3;
/*11*/ }
/*12*/ 
/*13*/ [RootSignature("DescriptorTable(UAV(u0,numDescriptors=2))")]
/*14*/ [numthreads(64,1,1)]
/*15*/ void main(uint3 dtid : SV_DispatchThreadID) {
/*16*/   float arg = u0[dtid.x];
/*17*/   float ret = foo(arg);
/*18*/   u1[dtid.x] = ret;
/*19*/ }

The default --report=debugger prints out the following:

Logical View
{File} example.o
{CompileUnit} example.hlsl
{Function}: main
  {Line}  example.hlsl:15 [main]
  {Line}  example.hlsl:16 [main]
  {Line}  example.hlsl:18 [main]
  {Line}  example.hlsl:16 [main]
  {Line}  example.hlsl:7 [foo][main:35]
  {Line}  example.hlsl:2 [bar][foo:22][main:35]
  {Line}  example.hlsl:18 [main]
  {Line}  example.hlsl:19 [main]

This allows for easy offline verification of specific inline call-stacks, line mapping order, etc.

Adding --print=symbols prints live variables for each {Line} statement:

Logical View
{File} example.o
{CompileUnit} example.hlsl
{Function} main
  {Line}  example.hlsl:15 [main]
  {Line}  example.hlsl:15 [main]
  {Line}  example.hlsl:16 [main]
    {Parameter} dtid: uint3 : regx VGPR0, piece 4 (line 15)
  {Line}  example.hlsl:18 [main]
    {Parameter} dtid: uint3 : regx VGPR0, piece 4 (line 15)
  {Line}  example.hlsl:16 [main]
    {Parameter} dtid: uint3 : regx VGPR0, piece 4 (line 15)
  {Line}  example.hlsl:7 [foo][main:17]
    {Parameter} dtid: uint3 : regx VGPR0, piece 4 (line 15)
    {Parameter} a: float : regx VGPR2 (line 6)
    {Variable} arg: float : regx VGPR2 (line 16)
  {Line}  example.hlsl:2 [bar][foo:9][main:17]
    {Parameter} dtid: uint3 : regx VGPR0, piece 4 (line 15)
    {Variable} my_var: float : regx VGPR2 (line 1)
  {Line}  example.hlsl:18 [main]
    {Parameter} dtid: uint3 : regx VGPR0, piece 4 (line 15)
    {Variable} ret: float : regx VGPR6 (line 17)
  {Line}  example.hlsl:19 [main]
    {Parameter} dtid: uint3 : regx VGPR0, piece 4 (line 15)
    {Variable} ret: float : regx VGPR6 (line 17)

Optionally, --print=instructions can be used to print out each instruction of the disasm to verify specific asm expressions for variable mappings.

Additionally, --attribute can be used to include things such as
offsets and scope levels for {Line} and {Instruction}.

@adam-yang adam-yang changed the title Added --check option to llvm-debuginfo-analyzer Added --debugger-view option to llvm-debuginfo-analyzer Sep 19, 2025
@github-actions
Copy link

github-actions bot commented Sep 19, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@adam-yang adam-yang marked this pull request as ready for review September 22, 2025 17:37
@llvmbot
Copy link
Member

llvmbot commented Sep 22, 2025

@llvm/pr-subscribers-backend-amdgpu

@llvm/pr-subscribers-debuginfo

Author: Adam Yang (adam-yang)

Changes

--debugger-view is a new mode for llvm-debuginfo-analyzer that prints out the debug info logical view in a way that matches more closely to the way a debugger. By default, --debugger-view prints out every statement line and its source location and call stack. Take this source for example:

/*01*/ float bar(float b) {
/*02*/   float ret = sin(b);
/*03*/   return ret;
/*04*/ }
/*05*/ 
/*06*/ float foo(float a) {
/*07*/   float my_var = sin(a);
/*08*/   float my_var2 = my_var * 3;
/*09*/   float my_var3 = bar(my_var2);
/*10*/   return my_var3;
/*11*/ }
/*12*/ 
/*13*/ [RootSignature("DescriptorTable(UAV(u0,numDescriptors=2))")]
/*14*/ [numthreads(64,1,1)]
/*15*/ void main(uint3 dtid : SV_DispatchThreadID) {
/*16*/   float arg = u0[dtid.x];
/*17*/   float ret = foo(arg);
/*18*/   u1[dtid.x] = ret;
/*19*/ }

The default --debugger-view prints out the following:

LINE:  [0x0000000000] example.hlsl:15 [main]
LINE:  [0x0000000020] example.hlsl:15 [main]
LINE:  [0x0000000050] example.hlsl:16 [main]
LINE:  [0x0000000068] example.hlsl:18 [main]
LINE:  [0x0000000070] example.hlsl:16 [main]
LINE:  [0x0000000080] example.hlsl:7 [foo][main:35]
LINE:  [0x000000009c] example.hlsl:2 [bar][foo:22][main:35]
LINE:  [0x00000000b0] example.hlsl:18 [main]
LINE:  [0x00000000cc] example.hlsl:19 [main]

This allows for easy offline verification of specific inline call-stacks, line mapping order, etc.

Another option --debugger-view-vars prints live variables for each LINE statement:

LINE:  [0x0000000000] example.hlsl:15 [main]
LINE:  [0x0000000020] example.hlsl:15 [main]
LINE:  [0x0000000050] example.hlsl:16 [main]
  VAR: dtid: uint3 : regx VGPR0, piece 4 (line 15)
LINE:  [0x0000000068] example.hlsl:18 [main]
  VAR: dtid: uint3 : regx VGPR0, piece 4 (line 15)
LINE:  [0x0000000070] example.hlsl:16 [main]
  VAR: dtid: uint3 : regx VGPR0, piece 4 (line 15)
LINE:  [0x0000000080] example.hlsl:7 [foo][main:17]
  VAR: dtid: uint3 : regx VGPR0, piece 4 (line 15)
  VAR: a: float : regx VGPR2 (line 6)
  VAR: arg: float : regx VGPR2 (line 16)
LINE:  [0x000000009c] example.hlsl:2 [bar][foo:9][main:17]
  VAR: dtid: uint3 : regx VGPR0, piece 4 (line 15)
  VAR: my_var: float : regx VGPR2 (line 1)
LINE:  [0x00000000b0] example.hlsl:18 [main]
  VAR: dtid: uint3 : regx VGPR0, piece 4 (line 15)
  VAR: ret: float : regx VGPR6 (line 17)
LINE:  [0x00000000cc] example.hlsl:19 [main]
  VAR: dtid: uint3 : regx VGPR0, piece 4 (line 15)
  VAR: ret: float : regx VGPR6 (line 17)

Optionally, --debugger-view-code can be used to print out each instruction of the disasm to verify specific asm expressions for variable mappings.


Patch is 22.21 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/159853.diff

7 Files Affected:

  • (modified) llvm/include/llvm/DebugInfo/LogicalView/Core/LVLocation.h (+2)
  • (modified) llvm/lib/DebugInfo/LogicalView/Core/LVLocation.cpp (+16-9)
  • (added) llvm/test/CodeGen/AMDGPU/amdgpu-debuginfo-check.ll (+110)
  • (modified) llvm/tools/llvm-debuginfo-analyzer/CMakeLists.txt (+1)
  • (added) llvm/tools/llvm-debuginfo-analyzer/DebuggerView.cpp (+272)
  • (added) llvm/tools/llvm-debuginfo-analyzer/DebuggerView.h (+37)
  • (modified) llvm/tools/llvm-debuginfo-analyzer/llvm-debuginfo-analyzer.cpp (+9-4)
diff --git a/llvm/include/llvm/DebugInfo/LogicalView/Core/LVLocation.h b/llvm/include/llvm/DebugInfo/LogicalView/Core/LVLocation.h
index 0718e33f5645b..c55b8259b4ee8 100644
--- a/llvm/include/llvm/DebugInfo/LogicalView/Core/LVLocation.h
+++ b/llvm/include/llvm/DebugInfo/LogicalView/Core/LVLocation.h
@@ -157,6 +157,7 @@ class LLVM_ABI LVLocation : public LVObject {
   void printRaw(raw_ostream &OS, bool Full = true) const;
   virtual void printRawExtra(raw_ostream &OS, bool Full = true) const {}
 
+  virtual void printLocations(raw_ostream &OS) const {}
   void print(raw_ostream &OS, bool Full = true) const override;
   void printExtra(raw_ostream &OS, bool Full = true) const override;
 };
@@ -177,6 +178,7 @@ class LLVM_ABI LVLocationSymbol final : public LVLocation {
                  uint64_t LocDescOffset) override;
   void addObject(LVSmall Opcode, ArrayRef<LVUnsigned> Operands) override;
 
+  void printLocations(raw_ostream &OS) const override;
   void printRawExtra(raw_ostream &OS, bool Full = true) const override;
   void printExtra(raw_ostream &OS, bool Full = true) const override;
 };
diff --git a/llvm/lib/DebugInfo/LogicalView/Core/LVLocation.cpp b/llvm/lib/DebugInfo/LogicalView/Core/LVLocation.cpp
index 3c078d8ee74b8..4724f4f007163 100644
--- a/llvm/lib/DebugInfo/LogicalView/Core/LVLocation.cpp
+++ b/llvm/lib/DebugInfo/LogicalView/Core/LVLocation.cpp
@@ -648,6 +648,19 @@ void LVLocation::print(LVLocations *Locations, raw_ostream &OS, bool Full) {
       Location->print(OS, Full);
 }
 
+void LVLocationSymbol::printLocations(raw_ostream &OS) const {
+  if (Entries) {
+    bool CodeViewLocation = getParentSymbol()->getHasCodeViewLocation();
+    std::string Leading;
+    for (LVOperation *Operation : *Entries) {
+      OS << Leading
+         << (CodeViewLocation ? Operation->getOperandsCodeViewInfo()
+                              : Operation->getOperandsDWARFInfo());
+      Leading = ", ";
+    }
+  }
+}
+
 void LVLocationSymbol::printExtra(raw_ostream &OS, bool Full) const {
   OS << "{Location}";
   if (getIsCallSite())
@@ -657,15 +670,9 @@ void LVLocationSymbol::printExtra(raw_ostream &OS, bool Full) const {
 
   // Print location entries.
   if (Full && Entries) {
-    bool CodeViewLocation = getParentSymbol()->getHasCodeViewLocation();
-    std::stringstream Stream;
-    std::string Leading;
-    for (LVOperation *Operation : *Entries) {
-      Stream << Leading
-             << (CodeViewLocation ? Operation->getOperandsCodeViewInfo()
-                                  : Operation->getOperandsDWARFInfo());
-      Leading = ", ";
-    }
+    std::string Str;
+    raw_string_ostream Stream(Str);
+    printLocations(Stream);
     printAttributes(OS, Full, "{Entry} ", const_cast<LVLocationSymbol *>(this),
                     StringRef(Stream.str()),
                     /*UseQuotes=*/false,
diff --git a/llvm/test/CodeGen/AMDGPU/amdgpu-debuginfo-check.ll b/llvm/test/CodeGen/AMDGPU/amdgpu-debuginfo-check.ll
new file mode 100644
index 0000000000000..56d6d3b9c9dbd
--- /dev/null
+++ b/llvm/test/CodeGen/AMDGPU/amdgpu-debuginfo-check.ll
@@ -0,0 +1,110 @@
+; RUN: llc %s -o %t.o -mcpu=gfx1030 -filetype=obj -O0
+; RUN: llvm-debuginfo-analyzer --debugger-view --debugger-view-vars %t.o | FileCheck %s
+
+; This test compiles this module with AMDGPU backend under -O0,
+; and makes sure llvm-debuginfo-check works for it.
+
+; CHECK: FUNCTION: main
+; CHECK: LINE: {{.+}}basic_var.hlsl:7
+; CHECK: LINE: {{.+}}basic_var.hlsl:11
+; CHECK: VAR: dtid: uint3 : reg{{.+}}, piece 4
+; CHECK: LINE: {{.+}}basic_var.hlsl:17
+; CHECK: VAR: dtid: uint3 : reg{{.+}}, piece 4
+; CHECK: LINE: {{.+}}basic_var.hlsl:11
+; CHECK: VAR: dtid: uint3 : reg{{.+}}, piece 4
+; CHECK: LINE: {{.+}}basic_var.hlsl:14
+; CHECK-DAG: VAR: dtid: uint3 : reg{{.+}}, piece 4
+; CHECK: LINE: {{.+}}basic_var.hlsl:17
+; CHECK-DAG: VAR: dtid: uint3 : reg{{.+}}, piece 4
+; CHECK-DAG: VAR: my_var2: float : reg{{.+}}
+; CHECK: LINE: {{.+}}basic_var.hlsl:19
+; CHECK-DAG: VAR: dtid: uint3 : reg{{.+}}, piece 4
+; CHECK-DAG: VAR: my_var2: float : reg{{.+}}
+
+source_filename = "module"
+target triple = "amdgcn-amd-amdpal"
+
+%dx.types.ResRet.f32 = type { float, float, float, float, i32 }
+
+; Function Attrs: memory(readwrite)
+define dllexport amdgpu_cs void @_amdgpu_cs_main(i32 inreg noundef %globalTable, i32 inreg noundef %userdata4, <3 x i32> inreg noundef %WorkgroupId, i32 inreg noundef %MultiDispatchInfo, <3 x i32> noundef %LocalInvocationId) #0 !dbg !14 {
+  %LocalInvocationId.i0 = extractelement <3 x i32> %LocalInvocationId, i64 0, !dbg !28
+  %WorkgroupId.i0 = extractelement <3 x i32> %WorkgroupId, i64 0, !dbg !28
+  %1 = call i64 @llvm.amdgcn.s.getpc(), !dbg !28
+  %2 = shl i32 %WorkgroupId.i0, 6, !dbg !28
+  %3 = add i32 %LocalInvocationId.i0, %2, !dbg !28
+    #dbg_value(i32 %3, !29, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !28)
+  %4 = and i64 %1, -4294967296, !dbg !30
+  %5 = zext i32 %userdata4 to i64, !dbg !30
+  %6 = or disjoint i64 %4, %5, !dbg !30
+  %7 = inttoptr i64 %6 to ptr addrspace(4), !dbg !30
+  call void @llvm.assume(i1 true) [ "align"(ptr addrspace(4) %7, i32 4), "dereferenceable"(ptr addrspace(4) %7, i32 -1) ], !dbg !30
+  %8 = load <4 x i32>, ptr addrspace(4) %7, align 4, !dbg !30, !invariant.load !2
+  %9 = call float @llvm.amdgcn.struct.buffer.load.format.f32(<4 x i32> %8, i32 %3, i32 0, i32 0, i32 0), !dbg !30
+    #dbg_value(%dx.types.ResRet.f32 poison, !31, !DIExpression(), !32)
+  %10 = fmul reassoc arcp contract afn float %9, 2.000000e+00, !dbg !33
+    #dbg_value(float %10, !34, !DIExpression(), !35)
+  call void @llvm.assume(i1 true) [ "align"(ptr addrspace(4) %7, i32 4), "dereferenceable"(ptr addrspace(4) %7, i32 -1) ], !dbg !36
+  %11 = getelementptr i8, ptr addrspace(4) %7, i64 32, !dbg !36
+  %.upto01 = insertelement <4 x float> poison, float %10, i64 0, !dbg !36
+  %12 = shufflevector <4 x float> %.upto01, <4 x float> poison, <4 x i32> zeroinitializer, !dbg !36
+  %13 = load <4 x i32>, ptr addrspace(4) %11, align 4, !dbg !36, !invariant.load !2
+  call void @llvm.amdgcn.struct.buffer.store.format.v4f32(<4 x float> %12, <4 x i32> %13, i32 %3, i32 0, i32 0, i32 0), !dbg !36
+  ret void, !dbg !37
+}
+
+declare noundef i64 @llvm.amdgcn.s.getpc() #1
+
+declare void @llvm.assume(i1 noundef) #2
+
+declare void @llvm.amdgcn.struct.buffer.store.format.v4f32(<4 x float>, <4 x i32>, i32, i32, i32, i32 immarg) #3
+
+declare float @llvm.amdgcn.struct.buffer.load.format.f32(<4 x i32>, i32, i32, i32, i32 immarg) #4
+
+attributes #0 = { memory(readwrite) }
+attributes #1 = { nocallback nofree nosync nounwind speculatable willreturn memory(none) }
+attributes #2 = { nocallback nofree nosync nounwind willreturn memory(inaccessiblemem: write) }
+attributes #3 = { nocallback nofree nosync nounwind willreturn memory(write) }
+attributes #4 = { nocallback nofree nosync nounwind willreturn memory(read) }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!12, !13}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !1, producer: "dxcoob 1.7.2308.16 (52da17e29)", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, enums: !2, globals: !3)
+!1 = !DIFile(filename: "tests\\basic_var.hlsl", directory: "")
+!2 = !{}
+!3 = !{!4, !10}
+!4 = distinct !DIGlobalVariableExpression(var: !5, expr: !DIExpression())
+!5 = !DIGlobalVariable(name: "u0", linkageName: "\01?u0@@3V?$RWBuffer@M@@A", scope: !0, file: !1, line: 2, type: !6, isLocal: false, isDefinition: true)
+!6 = !DICompositeType(tag: DW_TAG_class_type, name: "RWBuffer<float>", file: !1, line: 2, size: 32, align: 32, elements: !2, templateParams: !7)
+!7 = !{!8}
+!8 = !DITemplateTypeParameter(name: "element", type: !9)
+!9 = !DIBasicType(name: "float", size: 32, align: 32, encoding: DW_ATE_float)
+!10 = distinct !DIGlobalVariableExpression(var: !11, expr: !DIExpression())
+!11 = !DIGlobalVariable(name: "u1", linkageName: "\01?u1@@3V?$RWBuffer@M@@A", scope: !0, file: !1, line: 3, type: !6, isLocal: false, isDefinition: true)
+!12 = !{i32 2, !"Dwarf Version", i32 5}
+!13 = !{i32 2, !"Debug Info Version", i32 3}
+!14 = distinct !DISubprogram(name: "main", scope: !1, file: !1, line: 7, type: !15, scopeLine: 7, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0)
+!15 = !DISubroutineType(types: !16)
+!16 = !{null, !17}
+!17 = !DIDerivedType(tag: DW_TAG_typedef, name: "uint3", file: !1, baseType: !18)
+!18 = !DICompositeType(tag: DW_TAG_class_type, name: "vector<unsigned int, 3>", file: !1, size: 96, align: 32, elements: !19, templateParams: !24)
+!19 = !{!20, !22, !23}
+!20 = !DIDerivedType(tag: DW_TAG_member, name: "x", scope: !18, file: !1, baseType: !21, size: 32, align: 32, flags: DIFlagPublic)
+!21 = !DIBasicType(name: "unsigned int", size: 32, align: 32, encoding: DW_ATE_unsigned)
+!22 = !DIDerivedType(tag: DW_TAG_member, name: "y", scope: !18, file: !1, baseType: !21, size: 32, align: 32, offset: 32, flags: DIFlagPublic)
+!23 = !DIDerivedType(tag: DW_TAG_member, name: "z", scope: !18, file: !1, baseType: !21, size: 32, align: 32, offset: 64, flags: DIFlagPublic)
+!24 = !{!25, !26}
+!25 = !DITemplateTypeParameter(name: "element", type: !21)
+!26 = !DITemplateValueParameter(name: "element_count", type: !27, value: i32 3)
+!27 = !DIBasicType(name: "int", size: 32, align: 32, encoding: DW_ATE_signed)
+!28 = !DILocation(line: 7, column: 17, scope: !14)
+!29 = !DILocalVariable(name: "dtid", arg: 1, scope: !14, file: !1, line: 7, type: !17)
+!30 = !DILocation(line: 11, column: 18, scope: !14)
+!31 = !DILocalVariable(name: "my_var", scope: !14, file: !1, line: 11, type: !9)
+!32 = !DILocation(line: 11, column: 9, scope: !14)
+!33 = !DILocation(line: 14, column: 26, scope: !14)
+!34 = !DILocalVariable(name: "my_var2", scope: !14, file: !1, line: 14, type: !9)
+!35 = !DILocation(line: 14, column: 9, scope: !14)
+!36 = !DILocation(line: 17, column: 14, scope: !14)
+!37 = !DILocation(line: 19, column: 1, scope: !14)
diff --git a/llvm/tools/llvm-debuginfo-analyzer/CMakeLists.txt b/llvm/tools/llvm-debuginfo-analyzer/CMakeLists.txt
index 3e16d81abe35c..9de3078ffee55 100644
--- a/llvm/tools/llvm-debuginfo-analyzer/CMakeLists.txt
+++ b/llvm/tools/llvm-debuginfo-analyzer/CMakeLists.txt
@@ -15,4 +15,5 @@ set(LLVM_LINK_COMPONENTS
 add_llvm_tool(llvm-debuginfo-analyzer
   llvm-debuginfo-analyzer.cpp
   Options.cpp
+  DebuggerView.cpp
   )
diff --git a/llvm/tools/llvm-debuginfo-analyzer/DebuggerView.cpp b/llvm/tools/llvm-debuginfo-analyzer/DebuggerView.cpp
new file mode 100644
index 0000000000000..015096d49bd95
--- /dev/null
+++ b/llvm/tools/llvm-debuginfo-analyzer/DebuggerView.cpp
@@ -0,0 +1,272 @@
+//===-- DebuggerView.cpp ---------------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Options and functions related to --debugger-view for llvm-debuginfo-analyzer
+//
+//===----------------------------------------------------------------------===//
+
+#include "DebuggerView.h"
+#include "Options.h"
+#include "llvm/ADT/SetVector.h"
+#include "llvm/DebugInfo/LogicalView/LVReaderHandler.h"
+
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+
+using namespace llvm;
+using namespace debuggerview;
+
+cl::OptionCategory llvm::debuggerview::Category(
+    "Debugger View",
+    "Special printing mode that emulate how debugger uses debug info.");
+
+cl::opt<bool> llvm::debuggerview::Enable(
+    "debugger-view",
+    cl::desc("Enables debugger view. Normal debug-info printing is disabled "
+             "and options are ignored."),
+    cl::init(false), cl::cat(Category));
+static cl::opt<bool>
+    IncludeVars("debugger-view-vars",
+                cl::desc("Include live variables at each statement line."),
+                cl::init(false), cl::cat(Category));
+static cl::opt<bool>
+    IncludeCode("debugger-view-code",
+                cl::desc("Include disassembly at each statement line"),
+                cl::init(false), cl::cat(Category));
+static cl::opt<bool> IncludeRanges("debugger-view-ranges",
+                                   cl::desc("Include variable ranges"),
+                                   cl::init(false), cl::cat(Category));
+static cl::opt<bool> Help(
+    "debugger-view-help",
+    cl::desc(
+        "Print a detailed help screen about what kind of output to expect"),
+    cl::init(false), cl::cat(Category));
+
+constexpr const char *HelpText =
+    R"(Prints debug info in a way that is easy to verify correctness of debug info.
+FUNCTION: main
+  LINE: my_source_file.c:1 [main]       <---- New statement lines, inlined callstack
+    VAR: argc : int     : {expression}  <---- Variables live at this point
+    VAR: argv : char ** : {expression}
+  LINE: my_source_file.c:2 [main]
+    VAR: argc : int
+    VAR: argv : char **
+  LINE: my_source_file.c:3 [main]
+    VAR: argc : int
+    VAR: argv : char **
+  LINE: my_source_file.c:4 [main]
+)";
+
+using namespace llvm;
+using namespace logicalview;
+
+template <typename T>
+static T Take(Expected<T> ExpectedResult, const Twine &Msg) {
+  if (!ExpectedResult) {
+    auto Err = ExpectedResult.takeError();
+    errs() << Msg << " " << toStringWithoutConsuming(Err) << '\n';
+    exit(2);
+  }
+  T ret = std::move(*ExpectedResult);
+  return ret;
+}
+
+namespace {
+
+struct ScopePrinter {
+  std::vector<const LVLine *> Lines;
+  std::unordered_map<LVAddress, std::vector<const LVLocation *>> LivetimeBegins;
+  std::unordered_map<LVAddress, std::vector<const LVLocation *>>
+      LivetimeEndsExclusive;
+  raw_ostream &OS;
+
+  void Walk(raw_ostream &OS, const LVScope *Scope) {
+    if (Scope->scopeCount()) {
+      for (const LVScope *ChildScope : *Scope->getScopes())
+        Walk(OS, ChildScope);
+    }
+    if (Scope->lineCount()) {
+      for (const LVLine *Line : *Scope->getLines()) {
+        Lines.push_back(Line);
+      }
+    }
+    if (Scope->symbolCount()) {
+      for (const LVSymbol *Symbol : *Scope->getSymbols()) {
+        LVLocations SymbolLocations;
+        Symbol->getLocations(SymbolLocations);
+        if (SymbolLocations.empty())
+          continue;
+
+        if (IncludeRanges) {
+          OS << "RANGES: " << Symbol->getName() << " (line "
+             << Symbol->getLineNumber() << ")" << ": ";
+        }
+
+        for (const LVLocation *Loc : SymbolLocations) {
+          if (Loc->getIsGapEntry())
+            continue;
+
+          LVAddress Begin = Loc->getLowerAddress();
+          LVAddress End = Loc->getUpperAddress();
+          LivetimeBegins[Begin].push_back(Loc);
+          LivetimeEndsExclusive[End].push_back(Loc);
+          if (IncludeRanges) {
+            OS << "[" << hexValue(Begin) << ":" << hexValue(End) << "] ";
+          }
+        }
+
+        if (IncludeRanges)
+          OS << "\n";
+      }
+    }
+  }
+
+  ScopePrinter(raw_ostream &OS, const LVScopeFunction *Fn) : OS(OS) {
+    Walk(OS, Fn);
+    std::sort(Lines.begin(), Lines.end(),
+              [](const LVLine *a, const LVLine *b) -> bool {
+                if (a->getAddress() != b->getAddress())
+                  return a->getAddress() < b->getAddress();
+                if (a->getIsLineDebug() != b->getIsLineDebug())
+                  return a->getIsLineDebug();
+                return a->getID() < b->getID();
+              });
+  }
+
+  static void PrintIndent(raw_ostream &OS, int Indent) {
+    for (int i = 0; i < Indent; i++)
+      OS << "  ";
+  }
+
+  static void PrintCallstack(raw_ostream &OS, const LVScope *Scope) {
+    const LVScope *PrevScope = nullptr;
+    while (Scope) {
+      if (Scope->getIsFunction() || Scope->getIsInlinedFunction()) {
+        OS << "[" << Scope->getName();
+        if (PrevScope && PrevScope->getIsInlinedFunction()) {
+          OS << ":"
+             << cast<LVScopeFunctionInlined>(PrevScope)->getCallLineNumber();
+        }
+        OS << "]";
+        PrevScope = Scope;
+      }
+      Scope = Scope->getParentScope();
+    }
+  }
+
+  static bool IsChildScopeOf(const LVScope *A, const LVScope *B) {
+    while (A) {
+      A = A->getParentScope();
+      if (A == B)
+        return true;
+    }
+    return false;
+  }
+
+  void Print() {
+    SetVector<const LVLocation *>
+        LiveSymbols; // This needs to be ordered since we're iterating over it.
+    for (const LVLine *Line : Lines) {
+
+      const LVScope *Scope = Line->getParentScope();
+
+      // Update live list: Add lives
+      for (auto Loc : LivetimeBegins[Line->getAddress()])
+        LiveSymbols.insert(Loc);
+      // Update live list: remove dead
+      for (auto Loc : LivetimeEndsExclusive[Line->getAddress()])
+        LiveSymbols.remove(Loc);
+
+      if (Line->getIsNewStatement() && Line->getIsLineDebug() &&
+          Line->getLineNumber() != 0) {
+        auto LineDebug = cast<LVLineDebug>(Line);
+
+        OS << "LINE: " << " [" << hexValue(LineDebug->getAddress()) << "] "
+           << LineDebug->getPathname() << ":" << LineDebug->getLineNumber()
+           << " ";
+        PrintCallstack(OS, Scope);
+        OS << "\n";
+        if (IncludeVars) {
+          for (auto SymLoc : LiveSymbols) {
+            const LVSymbol *Sym = SymLoc->getParentSymbol();
+            auto SymScope = Sym->getParentScope();
+            auto LineScope = LineDebug->getParentScope();
+            if (SymScope != LineScope && !IsChildScopeOf(LineScope, SymScope))
+              continue;
+            PrintIndent(OS, 1);
+            OS << "VAR: " << Sym->getName() << ": " << Sym->getType()->getName()
+               << " : ";
+            SymLoc->printLocations(OS);
+            OS << " (line " << Sym->getLineNumber() << ")";
+            OS << "\n";
+          }
+        }
+
+      } else if (IncludeCode && Line->getIsLineAssembler()) {
+        OS << "  CODE: " << " [" << hexValue(Line->getAddress()) << "]  "
+           << Line->getName() << "\n";
+      }
+    }
+  }
+};
+
+} // namespace
+
+int llvm::debuggerview::printDebuggerView(std::vector<std::string> &Objects,
+                                          raw_ostream &OS) {
+  if (Help) {
+    OS << HelpText;
+    return EXIT_SUCCESS;
+  }
+
+  LVOptions Options;
+  Options.setAttributeAll();
+  Options.setAttributeAnyLocation();
+  Options.setPrintAll();
+  Options.setPrintAnyLine();
+  Options.resolveDependencies();
+
+  ScopedPrinter W(nulls());
+  LVReaderHandler Handler(Objects, W, Options);
+  std::vector<std::unique_ptr<LVReader>> Readers;
+  for (auto &Object : Objects) {
+    auto ExpectedReader = Handler.createReader(Object);
+    if (!ExpectedReader) {
+      auto Err = ExpectedReader.takeError();
+      errs() << "Failed to create reader: " << toStringWithoutConsuming(Err)
+             << '\n';
+      return 2;
+    }
+    Readers.emplace_back(std::move(*ExpectedReader));
+  }
+
+  for (auto &Reader : Readers) {
+    auto *CU = Reader->getCompileUnit();
+    if (!CU) {
+      errs() << "No compute unit found.\n";
+      return 2;
+    }
+
+    for (LVElement *Child : *CU->getChildren()) {
+      auto *Fn = dyn_cast<LVScopeFunction>(Child);
+      if (Fn) {
+        const LVLines *Lines = Fn->getLines();
+        // If there's no lines, this function has no body.
+        if (!Lines)
+          continue;
+        outs() << "FUNCTION: " << Child->getName() << "\n";
+
+        ScopePrinter P(OS, Fn);
+        P.Print();
+      }
+    }
+  }
+
+  return EXIT_SUCCESS;
+}
diff --git a/llvm/tools/llvm-debuginfo-analyzer/DebuggerView.h b/llvm/tools/llvm-debuginfo-analyzer/DebuggerView.h
new file mode 100644
index 0000000000000..d775fa0c80d2d
--- /dev/null
+++ b/llvm/tools/llvm-debuginfo-analyzer/DebuggerView.h
@@ -0,0 +1,37 @@
+//===-- DebuggerView.h ------------------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Options and functions related to --debugger-view for llvm-debuginfo-analyzer
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef DEBUGGER_VIEW_H
+#define DEBUGGER_VIEW_H
+
+#include "l...
[truncated]

@CarlosAlbertoEnciso
Copy link
Member

CarlosAlbertoEnciso commented Sep 25, 2025

@adam-yang, The concept of the debugger view is very interesting. Thanks for doing this.
Without looking in detail at the specific details, these are some points that should be changed to fit with the tool/library framework:

  • Instead of introducing new descriptors for the logical line and symbol
LINE:  [0x0000000050] example.hlsl:16 [main]
  VAR: dtid: uint3 : regx VGPR0, piece 4 (line 15)

we should keep the existing ones: {Line} and {Variable}

{Line}:  [0x0000000050] example.hlsl:16 [main]
  {Variable}: dtid: uint3 : regx VGPR0, piece 4 (line 15)
  • The debugger view should be available via the LogicalView library and not via the llvm-debuginfo-analyzer tool.
    Basically the proposed changes should be moved to llvm/lib/DebugInfo/LogicalView/Core. In that way any consumer of that library will have access to the new view.

  • The new options should be treated in the same way (location and checks) as the existing options.
    Be in llvm/tools/llvm-debuginfo-analyzer/Options.cpp and updated in LVOptions::resolveDependencies

  • May be add the new options to an existing category: --report or --output.
    Control the element included via the --print=xxx

--debugger-view       --> --report=debugger` or `--output=debugger
--debugger-view-vars  --> --print=symbols
--debugger-view-code  --> --print=instructions
  • The general documentation llvm/docs/CommandGuide/llvm-debuginfo-analyzer.rst needs to be updated.

Copy link
Contributor

@jalopezg-git jalopezg-git left a comment

Choose a reason for hiding this comment

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

Thank you! In addition to @CarlosAlbertoEnciso's comments, here are a couple more suggestions / additional comments.

@adam-yang adam-yang changed the title Added --debugger-view option to llvm-debuginfo-analyzer Added --report=debugger option to llvm-debuginfo-analyzer Sep 30, 2025
@adam-yang
Copy link
Contributor Author

@adam-yang, The concept of the debugger view is very interesting. Thanks for doing this. Without looking in detail at the specific details, these are some points that should be changed to fit with the tool/library framework:

  • Instead of introducing new descriptors for the logical line and symbol
LINE:  [0x0000000050] example.hlsl:16 [main]
  VAR: dtid: uint3 : regx VGPR0, piece 4 (line 15)

we should keep the existing ones: {Line} and {Variable}

{Line}:  [0x0000000050] example.hlsl:16 [main]
  {Variable}: dtid: uint3 : regx VGPR0, piece 4 (line 15)
  • The debugger view should be available via the LogicalView library and not via the llvm-debuginfo-analyzer tool.
    Basically the proposed changes should be moved to llvm/lib/DebugInfo/LogicalView/Core. In that way any consumer of that library will have access to the new view.
  • The new options should be treated in the same way (location and checks) as the existing options.
    Be in llvm/tools/llvm-debuginfo-analyzer/Options.cpp and updated in LVOptions::resolveDependencies
  • May be add the new options to an existing category: --report or --output.
    Control the element included via the --print=xxx
--debugger-view       --> --report=debugger` or `--output=debugger
--debugger-view-vars  --> --print=symbols
--debugger-view-code  --> --print=instructions
  • The general documentation llvm/docs/CommandGuide/llvm-debuginfo-analyzer.rst needs to be updated.

Made all the requested changes.

@adam-yang adam-yang force-pushed the amdgpu-debuginfo-check branch from 895983b to 431ec62 Compare October 1, 2025 23:36
REPORT_OPTION(Children);
REPORT_OPTION(List);
REPORT_OPTION(Parents);
REPORT_OPTION(Debugger);
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 preserve the alphabetical order?

auto *CU = getCompileUnit();
if (!CU)
return createStringError(std::make_error_code(std::errc::invalid_argument),
"Error: No compute unit found.");
Copy link
Member

Choose a reason for hiding this comment

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

May be: compile unit?

"Selected elements are displayed in a tree view "
"(Include parents and children.")));
"(Include parents and children."),
clEnumValN(
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 preserve the alphabetical order?

} // namespace

Error LVReader::printDebugger() {
auto *CU = getCompileUnit();
Copy link
Member

@CarlosAlbertoEnciso CarlosAlbertoEnciso Oct 9, 2025

Choose a reason for hiding this comment

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

Have you considered the case of multiple CUs in the same input file?
The CU returned by getCompileUnit() points to the last CU processed.
I would suggest to start with the Scope Root and traverse its children that are Compile Units.

for (const LVScopeCompileUnit *CU : Root->getScopes()) {
  for (const LVScope *ChildScope : *CU->getScopes()) {
    ..
  }
}

In that way, the debugger view is generated for all CUs in the input file.

The combined **debugger** layout prints each statement line in order and
variables live at each line (if `--print=symbols` given), as well as
instructions (if `--print=instructions` given).

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 add a section between SELECT LOGICAL ELEMENTS and COMPARISON MODE that shows the debugger view, using one of the input files located in llvm/test/tools/llvm-debuginfo-analyzer. First using the standard logical view and then the debugger view.
Something like:

DEBUGGER VIEW
"""""""""""""
The following prints ... logical view ...

.. code-block:: none

  llvm-debuginfo-analyzer --print=symbols test-dwarf-clang.o

  the generated logical view ...  

The following prints ... debugger view ...

.. code-block:: none

  llvm-debuginfo-analyzer --report=debugger --print=symbols test-dwarf-clang.o

  the generated debugger view ...  

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added this section.

.. code-block:: none
llvm-debuginfo-analyzer --report=debugger
test-dwarf-clang.o test-dwarf-gcc.o

Choose a reason for hiding this comment

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

For consistency with the another general printing layouts, include:
Logical View title follow by {File and CompileUnit entries

Logical View:
 [000]           {File} 'test-dwarf-clang.o'
 [001]           {CompileUnit} 'test.cpp'

statement line is printed out in order, complete with the inlined
callstack. This is useful to verify the specific orders of lines, as
well as verifying inline callstacks.

Copy link
Member

@CarlosAlbertoEnciso CarlosAlbertoEnciso Oct 20, 2025

Choose a reason for hiding this comment

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

For consistency with other View layouts, the Debugger View should support additional attributes such as --attribute=offset,level.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added support for them. In fact the offsets and levels are controlled by the same mechanism for printing them as the other options.

return Error::success();

for (auto *Scope : *Root->getScopes()) {
auto *CU = dyn_cast<LVScopeCompileUnit>(Scope);
Copy link
Member

@CarlosAlbertoEnciso CarlosAlbertoEnciso Oct 20, 2025

Choose a reason for hiding this comment

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

The Root has only CompileUnit children. Use the inheritance relationships to invoke the needed printing code. Have a look at LVScopeRoot::doPrintMatches to see how to handle the output split feature.

Error LVScopeRoot::doPrintDebugger(bool Split, raw_ostream &OS, ....) const {
  if (Scopes) {
    ...
    for (LVScope *Scope : *Scopes) {
      // Virtual function in LVScope and defined for:
      // LVScopeCompileUnit, LVScopeFunction and LVScopeFunctionInlined
      Scope->printDebugger(.....);
    }
    ...
}
void LVScopeCompileUnit::printDebugger(raw_ostream &OS, ...) {
  ...
}
LVScopeFunction::printDebugger(raw_ostream &OS, ...) {
  ...
}
LVScopeFunctionInlined::printDebugger(raw_ostream &OS, ...) {
  ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. The bulk of the logic is still in one place though, under LVFunction::printDebugger, but much of the specifics have been moved to be handled by virtual functions.

@CarlosAlbertoEnciso
Copy link
Member

FYI: There is a PR #69175 to move all the printing support to a common function.

@CarlosAlbertoEnciso
Copy link
Member

@adam-yang Thanks for updating your patch. I will have a look at it in the next couple of days.

@jalopezg-git
Copy link
Contributor

@adam-yang, @CarlosAlbertoEnciso I could try to take another look next week, if needed 🙂.

@CarlosAlbertoEnciso
Copy link
Member

Can you include a test for the new option.

<< Symbol->getLineNumber() << ")" << ": ";

for (const LVLocation *Loc : SymbolLocations) {
if (Loc->getIsGapEntry())

Choose a reason for hiding this comment

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

Loc --> Location

if (getPrintSymbols())
setAttributeRange();
}

Choose a reason for hiding this comment

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

Just a very minor points. Missing full stops in the comments.

}

void LVLocationSymbol::printDebugger(raw_ostream &OS, LVLevel Indent) const {
LVSymbol *Sym = getParentSymbol();

Choose a reason for hiding this comment

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

For consistency: Instead of Sym use Symbol

LifetimeBegins[Begin].push_back(Loc);
LifetimeEndsExclusive[End].push_back(Loc);
}
}

Choose a reason for hiding this comment

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

For consistency: Loc --> Location

if (Scope->scopeCount()) {
for (const LVScope *ChildScope : *Scope->getScopes())
Worklist.push_back(ChildScope);
}

Choose a reason for hiding this comment

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

if (Scope->scopeCount()) {
  for (const LVScope *ChildScope : *Scope->getScopes())
    Worklist.push_back(ChildScope);
}

Can that be replaced with something like:

Worklist.insert(Worklist.end(), *Scope->getScopes()->begin(), *Scope->getScopes()->end());

for (const LVLine *Line : *Scope->getLines()) {
AllLines.push_back(Line);
}
}

Choose a reason for hiding this comment

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

Same change here?

{Line} test.cpp:3 [foo]
{Parameter} ParamBool: bool : fbreg -21 (line 2)
{Parameter} ParamPtr: INTPTR : fbreg -16 (line 2)
{Parameter} ParamUnsigned: unsigned int : fbreg -20 (line 2)
Copy link
Member

@CarlosAlbertoEnciso CarlosAlbertoEnciso Oct 29, 2025

Choose a reason for hiding this comment

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

The format looks good, but I have some questions/suggestions.

Logical View:
  {File} test-dwarf-clang.o
  {CompileUnit} test.cpp
  {Function} foo
    {Line}  test.cpp:2 [foo]
      {Parameter} ParamBool: bool : fbreg -21 (line 2)
      {Parameter} ParamPtr: INTPTR : fbreg -16 (line 2)
      {Parameter} ParamUnsigned: unsigned int : fbreg -20 (line 2)
    {Line}  test.cpp:3 [foo]
      {Parameter} ParamBool: bool : fbreg -21 (line 2)
      {Parameter} ParamPtr: INTPTR : fbreg -16 (line 2)
      {Parameter} ParamUnsigned: unsigned int : fbreg -20 (line 2)

As you are using indentation to describe the lexical scope, to keep consistency, the {CompileUnit} and {Function} should have different indentation as the {File}. Also, any string (type name, symbol name) are enclosed by single quotes.

Logical View:
{File} 'test-dwarf-clang.o'
                                <-- blank line
  {CompileUnit} 'test.cpp'
    {Function} 'foo' -> 'int'   <-- type

Any specific reasons for the symbol location to be on the same line as the symbol information. For optimized code, you can have multiple location entries. That is why we print the locations in a different lexical scope:

{Parameter} 'ParamBool' -> 'bool' : fbreg -21 (line 2)
-->
{Parameter} 'ParamBool' -> 'bool' (line 2)
  {Location}
    {Entry} fbreg -21    

@CarlosAlbertoEnciso
Copy link
Member

@adam-yang, @CarlosAlbertoEnciso I could try to take another look next week, if needed 🙂.

@jalopezg-git That would be great.
As we are introducing a new view layout, we should agreed on the general format, so new consumers will get a real benefit. I can assume that a third-party tool can use the logical library to validate the debug information using the new debugger view.

Copy link
Contributor

@jalopezg-git jalopezg-git left a comment

Choose a reason for hiding this comment

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

Thanks a lot for the contribution, @adam-yang! I managed to squeeze the review in the 'lunch' slot in WG21 meeting 🙂.

This looks very nice! I have attached some minor comments that add to those of @CarlosAlbertoEnciso.

Comment on lines +654 to +660
std::string Leading;
for (LVOperation *Operation : *Entries) {
OS << Leading
<< (CodeViewLocation ? Operation->getOperandsCodeViewInfo()
: Operation->getOperandsDWARFInfo());
Leading = ", ";
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I see that this code was moved from this original location, but perhaps we could take the chance to adopt llvm::interleave() to print to OS w/ interleaved ,s.

void LVLocationSymbol::printDebugger(raw_ostream &OS, LVLevel Indent) const {
LVSymbol *Sym = getParentSymbol();
OS << indentAsString(Indent) << formattedKind(Sym->kind());
OS << " " << Sym->getName() << ": " << Sym->getType()->getName() << " : ";
Copy link
Contributor

Choose a reason for hiding this comment

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

IIRC, getType() may return nullptr under certain circumstances (e.g., for manually-forged or otherwise incomplete DWARF input). I think we should check for non-null here before dereferencing.

clEnumValN(
LVReportKind::Debugger, "debugger",
"Selected elements are displayed in a simulated debugger view "
"(Include parents and children."),
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"(Include parents and children."),
"(Include parents and children)"),

{Line} test.cpp:8 [foo]
{Line} test.cpp:9 [foo]
Optionally, by adding `--print=symbols`, live variables for each line is
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Optionally, by adding `--print=symbols`, live variables for each line is
Optionally, by adding `--print=symbols`, live variables for each line are

Comment on lines +1973 to +1977
if (a->getAddress() != b->getAddress())
return a->getAddress() < b->getAddress();
if (a->getIsLineDebug() != b->getIsLineDebug())
return a->getIsLineDebug();
return a->getID() < b->getID();
Copy link
Contributor

Choose a reason for hiding this comment

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

Possibly, this could use the suggestion below (unsure if it preserves the intended ordering for a->getIsLineDebug() though). WDYT, @adam-yang?

Suggested change
if (a->getAddress() != b->getAddress())
return a->getAddress() < b->getAddress();
if (a->getIsLineDebug() != b->getIsLineDebug())
return a->getIsLineDebug();
return a->getID() < b->getID();
return std::tie(a->getAddress(), a->getIsLineDebug(), a->getID())
< std::tie(b->getAddress(), b->getIsLineDebug(), b->getID());

Comment on lines +1986 to +1991
// Update live list: Add lives
for (auto Loc : LifetimeBegins[Line->getAddress()])
LiveSymbols.insert(Loc);
// Update live list: remove dead
for (auto Loc : LifetimeEndsExclusive[Line->getAddress()])
LiveSymbols.remove(Loc);
Copy link
Contributor

Choose a reason for hiding this comment

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

IIUC, LiveSymbols content is only consumed if IncludeVars evaluates to true, i.e. we could potentially skip updating it when not required.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants