[llvm] [llvm-dwarfdump][LineCov 1/3] Add variable coverage metrics (PR #176725)

via llvm-commits llvm-commits at lists.llvm.org
Mon Jan 19 02:38:26 PST 2026


https://github.com/unexpectedlydefined created https://github.com/llvm/llvm-project/pull/176725

Patch 1 of 3 to add to llvm-dwarfdump the ability to measure DWARF coverage of local variables in terms of source lines, as discussed in [this RFC](https://discourse.llvm.org/t/rfc-debug-info-coverage-tool-v2/83266).

This patch adds the basic variable coverage implementation. By default, inlined instances are shown separately (displaying the full inlining chain). Alternatively, a combined view that averages across all inlined instances can be returned using `--combine-instances`.

In this patch, we simply print a count of source lines over which each variable is covered. Later patches in the series will add the comparison against a baseline.

Example output:
```
$ llvm-dwarfdump --show-variable-coverage somefile
Variable coverage statistics:
Function InlChain                   Variable Decl                  LinesCovered
foo                                 bar      path/to/somefile.h:54 3
foo      path/to/someotherfile.c:32 bar      path/to/somefile.h:54 2
foo                                 baz      main.c:76             9
```

```
$ llvm-dwarfdump --show-variable-coverage somefile --combine-instances
Variable coverage statistics:
Function InstanceCount Variable Decl                  LinesCovered
foo      2             bar      path/to/somefile.h:54 2.5
foo      1             baz      main.c:76             9
```

This reapplies #169646, fixing some ambiguous overloads that caused several bots to fail.

>From b7e41891eecdeff393e1970e3f7b68dc48db0cab Mon Sep 17 00:00:00 2001
From: unexpectedlydefined <unexpectedlydefined at gmail.com>
Date: Mon, 19 Jan 2026 10:05:06 +0000
Subject: [PATCH] Add variable coverage metrics

---
 llvm/docs/CommandGuide/llvm-dwarfdump.rst     |  39 +++
 .../llvm-dwarfdump/X86/Inputs/coverage-opt.ll | 148 +++++++++++
 .../llvm-dwarfdump/X86/Inputs/coverage.ll     | 180 +++++++++++++
 .../tools/llvm-dwarfdump/X86/coverage.test    |  26 ++
 llvm/tools/llvm-dwarfdump/CMakeLists.txt      |   1 +
 llvm/tools/llvm-dwarfdump/Coverage.cpp        | 241 ++++++++++++++++++
 llvm/tools/llvm-dwarfdump/llvm-dwarfdump.cpp  |  21 +-
 llvm/tools/llvm-dwarfdump/llvm-dwarfdump.h    |   2 +
 8 files changed, 657 insertions(+), 1 deletion(-)
 create mode 100644 llvm/test/tools/llvm-dwarfdump/X86/Inputs/coverage-opt.ll
 create mode 100644 llvm/test/tools/llvm-dwarfdump/X86/Inputs/coverage.ll
 create mode 100644 llvm/test/tools/llvm-dwarfdump/X86/coverage.test
 create mode 100644 llvm/tools/llvm-dwarfdump/Coverage.cpp

diff --git a/llvm/docs/CommandGuide/llvm-dwarfdump.rst b/llvm/docs/CommandGuide/llvm-dwarfdump.rst
index dfc0431f07826..86bdfd6f5224e 100644
--- a/llvm/docs/CommandGuide/llvm-dwarfdump.rst
+++ b/llvm/docs/CommandGuide/llvm-dwarfdump.rst
@@ -193,6 +193,17 @@ OPTIONS
 
             The :option:`--debug-frame` and :option:`--eh-frame` options are aliases, in cases where both sections are present one command outputs both.
 
+.. option:: --show-variable-coverage
+
+            Show per-variable coverage metrics. The output format is described
+            in the section below (:ref:`variable-coverage-format`).
+
+.. option:: --combine-inline-variable-instances
+
+            Use with :option:`--show-variable-coverage` to average variable
+            coverage across inlined subroutine instances instead of printing
+            them separately.
+
 .. option:: @<FILE>
 
             Read command-line options from `<FILE>`.
@@ -243,6 +254,34 @@ The following is generated if there are no errors reported::
     "error-count": 0
   }
 
+.. _variable-coverage-format:
+
+FORMAT OF VARIABLE COVERAGE OUTPUT
+----------------------------------
+
+The :option:`--show-variable-coverage` option differs from
+:option:`--statistics` by printing per-variable debug info coverage metrics
+based on the number of source lines covered instead of the number of
+instruction bytes. Compared to counting instruction bytes, this is more stable
+across compilations and better reflects the debugging experience. The output is
+a tab-separated table containing the following columns:
+
+      - `Function` ==> Name of the function the variable was found in
+      - `InstanceCount` (when :option:`--combine-inline-variable-instances` is
+        specified) ==> Number of instances of the function; this is 1 for
+        functions that have not been inlined, and n+1 for functions that have
+        been inlined n times
+      - `InlChain` (when :option:`--combine-inline-variable-instances` is not
+        specified) ==> Chain of call sites (file and line number) that the
+        function has been inlined into; this will be empty if the function has
+        not been inlined
+      - `Variable` ==> Name of the variable
+      - `Decl` ==> Source location (file and line number) of the variable's
+        declaration
+      - `LinesCovered` ==> Number of source lines covered by the variable's
+        debug information in the input file
+
+
 EXIT STATUS
 -----------
 
diff --git a/llvm/test/tools/llvm-dwarfdump/X86/Inputs/coverage-opt.ll b/llvm/test/tools/llvm-dwarfdump/X86/Inputs/coverage-opt.ll
new file mode 100644
index 0000000000000..e765e49db1d18
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfdump/X86/Inputs/coverage-opt.ll
@@ -0,0 +1,148 @@
+; The source code of the test case:
+; extern void fn3(int *);
+; extern void fn2(int);
+; __attribute__((noinline)) void fn1(int x, int y) {
+;   int u = x + y;
+;   if (x > 1)
+;     u += 1;
+;   else
+;     u += 2;
+;   if (y > 4)
+;     u += x;
+;   int a = 7;
+;   fn2(a);
+;   int v = u;
+;   v++;
+;   u--;
+;   fn2(u);
+; }
+; 
+; __attribute__((noinline)) int f() {
+;   int l, k;
+;   fn3(&l);
+;   fn3(&k);
+;   fn1(l, k);
+;   return 0;
+; }
+
+; ModuleID = 'test.c'
+source_filename = "test.c"
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+; Function Attrs: noinline nounwind uwtable
+define dso_local void @fn1(i32 noundef %0, i32 noundef %1) local_unnamed_addr !dbg !10 {
+    #dbg_value(i32 %0, !15, !DIExpression(), !20)
+    #dbg_value(i32 %1, !16, !DIExpression(), !20)
+    #dbg_value(i32 poison, !17, !DIExpression(), !20)
+  %3 = icmp sgt i32 %0, 1, !dbg !21
+  %4 = select i1 %3, i32 1, i32 2, !dbg !23
+    #dbg_value(i32 poison, !17, !DIExpression(), !20)
+  %5 = icmp sgt i32 %1, 4, !dbg !24
+  %6 = select i1 %5, i32 %0, i32 0, !dbg !26
+    #dbg_value(i32 poison, !17, !DIExpression(), !20)
+    #dbg_value(i32 7, !18, !DIExpression(), !20)
+  tail call void @fn2(i32 noundef 7), !dbg !27
+    #dbg_value(i32 poison, !19, !DIExpression(), !20)
+    #dbg_value(i32 poison, !19, !DIExpression(DW_OP_plus_uconst, 1, DW_OP_stack_value), !20)
+  %7 = add i32 %0, -1, !dbg !28
+  %8 = add i32 %7, %1, !dbg !23
+  %9 = add i32 %8, %4, !dbg !26
+  %10 = add i32 %9, %6, !dbg !29
+    #dbg_value(i32 %10, !17, !DIExpression(), !20)
+  tail call void @fn2(i32 noundef %10), !dbg !30
+  ret void, !dbg !31
+}
+
+; Function Attrs: mustprogress nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
+declare void @llvm.lifetime.start.p0(i64 immarg, ptr nocapture)
+
+declare !dbg !32 void @fn2(i32 noundef) local_unnamed_addr
+
+; Function Attrs: mustprogress nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
+declare void @llvm.lifetime.end.p0(i64 immarg, ptr nocapture)
+
+; Function Attrs: noinline nounwind uwtable
+define dso_local noundef i32 @f() local_unnamed_addr !dbg !35 {
+  %1 = alloca i32, align 4, !DIAssignID !41
+    #dbg_assign(i1 poison, !39, !DIExpression(), !41, ptr %1, !DIExpression(), !42)
+  %2 = alloca i32, align 4, !DIAssignID !43
+    #dbg_assign(i1 poison, !40, !DIExpression(), !43, ptr %2, !DIExpression(), !42)
+  call void @llvm.lifetime.start.p0(i64 4, ptr nonnull %1), !dbg !44
+  call void @llvm.lifetime.start.p0(i64 4, ptr nonnull %2), !dbg !44
+  call void @fn3(ptr noundef nonnull %1), !dbg !45
+  call void @fn3(ptr noundef nonnull %2), !dbg !46
+  %3 = load i32, ptr %1, align 4, !dbg !47, !tbaa !48
+  %4 = load i32, ptr %2, align 4, !dbg !52, !tbaa !48
+  call void @fn1(i32 noundef %3, i32 noundef %4), !dbg !53
+  call void @llvm.lifetime.end.p0(i64 4, ptr nonnull %2), !dbg !54
+  call void @llvm.lifetime.end.p0(i64 4, ptr nonnull %1), !dbg !54
+  ret i32 0, !dbg !55
+}
+
+declare !dbg !56 void @fn3(ptr noundef) local_unnamed_addr
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3, !4, !5, !6, !7, !8}
+!llvm.ident = !{!9}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !1, producer: "clang version 19.1.7", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: None)
+!1 = !DIFile(filename: "test.c", directory: "/")
+!2 = !{i32 7, !"Dwarf Version", i32 5}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!4 = !{i32 1, !"wchar_size", i32 4}
+!5 = !{i32 8, !"PIC Level", i32 2}
+!6 = !{i32 7, !"PIE Level", i32 2}
+!7 = !{i32 7, !"uwtable", i32 2}
+!8 = !{i32 7, !"debug-info-assignment-tracking", i1 true}
+!9 = !{!"clang version 19.1.7"}
+!10 = distinct !DISubprogram(name: "fn1", scope: !1, file: !1, line: 3, type: !11, scopeLine: 3, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !14)
+!11 = !DISubroutineType(types: !12)
+!12 = !{null, !13, !13}
+!13 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!14 = !{!15, !16, !17, !18, !19}
+!15 = !DILocalVariable(name: "x", arg: 1, scope: !10, file: !1, line: 3, type: !13)
+!16 = !DILocalVariable(name: "y", arg: 2, scope: !10, file: !1, line: 3, type: !13)
+!17 = !DILocalVariable(name: "u", scope: !10, file: !1, line: 4, type: !13)
+!18 = !DILocalVariable(name: "a", scope: !10, file: !1, line: 11, type: !13)
+!19 = !DILocalVariable(name: "v", scope: !10, file: !1, line: 13, type: !13)
+!20 = !DILocation(line: 0, scope: !10)
+!21 = !DILocation(line: 5, column: 9, scope: !22)
+!22 = distinct !DILexicalBlock(scope: !10, file: !1, line: 5, column: 7)
+!23 = !DILocation(line: 5, column: 7, scope: !10)
+!24 = !DILocation(line: 9, column: 9, scope: !25)
+!25 = distinct !DILexicalBlock(scope: !10, file: !1, line: 9, column: 7)
+!26 = !DILocation(line: 9, column: 7, scope: !10)
+!27 = !DILocation(line: 12, column: 3, scope: !10)
+!28 = !DILocation(line: 4, column: 13, scope: !10)
+!29 = !DILocation(line: 15, column: 4, scope: !10)
+!30 = !DILocation(line: 16, column: 3, scope: !10)
+!31 = !DILocation(line: 17, column: 1, scope: !10)
+!32 = !DISubprogram(name: "fn2", scope: !1, file: !1, line: 2, type: !33, flags: DIFlagPrototyped, spFlags: DISPFlagOptimized)
+!33 = !DISubroutineType(types: !34)
+!34 = !{null, !13}
+!35 = distinct !DISubprogram(name: "f", scope: !1, file: !1, line: 19, type: !36, scopeLine: 19, flags: DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !38)
+!36 = !DISubroutineType(types: !37)
+!37 = !{!13}
+!38 = !{!39, !40}
+!39 = !DILocalVariable(name: "l", scope: !35, file: !1, line: 20, type: !13)
+!40 = !DILocalVariable(name: "k", scope: !35, file: !1, line: 20, type: !13)
+!41 = distinct !DIAssignID()
+!42 = !DILocation(line: 0, scope: !35)
+!43 = distinct !DIAssignID()
+!44 = !DILocation(line: 20, column: 3, scope: !35)
+!45 = !DILocation(line: 21, column: 3, scope: !35)
+!46 = !DILocation(line: 22, column: 3, scope: !35)
+!47 = !DILocation(line: 23, column: 7, scope: !35)
+!48 = !{!49, !49, i64 0}
+!49 = !{!"int", !50, i64 0}
+!50 = !{!"omnipotent char", !51, i64 0}
+!51 = !{!"Simple C/C++ TBAA"}
+!52 = !DILocation(line: 23, column: 10, scope: !35)
+!53 = !DILocation(line: 23, column: 3, scope: !35)
+!54 = !DILocation(line: 25, column: 1, scope: !35)
+!55 = !DILocation(line: 24, column: 3, scope: !35)
+!56 = !DISubprogram(name: "fn3", scope: !1, file: !1, line: 1, type: !57, flags: DIFlagPrototyped, spFlags: DISPFlagOptimized)
+!57 = !DISubroutineType(types: !58)
+!58 = !{null, !59}
+!59 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !13, size: 64)
diff --git a/llvm/test/tools/llvm-dwarfdump/X86/Inputs/coverage.ll b/llvm/test/tools/llvm-dwarfdump/X86/Inputs/coverage.ll
new file mode 100644
index 0000000000000..aa7dd2f89c855
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfdump/X86/Inputs/coverage.ll
@@ -0,0 +1,180 @@
+; The source code of the test case:
+; extern void fn3(int *);
+; extern void fn2(int);
+; __attribute__((noinline)) void fn1(int x, int y) {
+;   int u = x + y;
+;   if (x > 1)
+;     u += 1;
+;   else
+;     u += 2;
+;   if (y > 4)
+;     u += x;
+;   int a = 7;
+;   fn2(a);
+;   int v = u;
+;   v++;
+;   u--;
+;   fn2(u);
+; }
+; 
+; __attribute__((noinline)) int f() {
+;   int l, k;
+;   fn3(&l);
+;   fn3(&k);
+;   fn1(l, k);
+;   return 0;
+; }
+
+; ModuleID = 'test.c'
+source_filename = "test.c"
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+; Function Attrs: noinline nounwind optnone uwtable
+define dso_local void @fn1(i32 noundef %0, i32 noundef %1) !dbg !10 {
+  %3 = alloca i32, align 4
+  %4 = alloca i32, align 4
+  %5 = alloca i32, align 4
+  %6 = alloca i32, align 4
+  %7 = alloca i32, align 4
+  store i32 %0, ptr %3, align 4
+    #dbg_declare(ptr %3, !15, !DIExpression(), !16)
+  store i32 %1, ptr %4, align 4
+    #dbg_declare(ptr %4, !17, !DIExpression(), !18)
+    #dbg_declare(ptr %5, !19, !DIExpression(), !20)
+  %8 = load i32, ptr %3, align 4, !dbg !21
+  %9 = load i32, ptr %4, align 4, !dbg !22
+  %10 = add nsw i32 %8, %9, !dbg !23
+  store i32 %10, ptr %5, align 4, !dbg !20
+  %11 = load i32, ptr %3, align 4, !dbg !24
+  %12 = icmp sgt i32 %11, 1, !dbg !26
+  br i1 %12, label %13, label %16, !dbg !27
+
+13:                                               ; preds = %2
+  %14 = load i32, ptr %5, align 4, !dbg !28
+  %15 = add nsw i32 %14, 1, !dbg !28
+  store i32 %15, ptr %5, align 4, !dbg !28
+  br label %19, !dbg !29
+
+16:                                               ; preds = %2
+  %17 = load i32, ptr %5, align 4, !dbg !30
+  %18 = add nsw i32 %17, 2, !dbg !30
+  store i32 %18, ptr %5, align 4, !dbg !30
+  br label %19
+
+19:                                               ; preds = %16, %13
+  %20 = load i32, ptr %4, align 4, !dbg !31
+  %21 = icmp sgt i32 %20, 4, !dbg !33
+  br i1 %21, label %22, label %26, !dbg !34
+
+22:                                               ; preds = %19
+  %23 = load i32, ptr %3, align 4, !dbg !35
+  %24 = load i32, ptr %5, align 4, !dbg !36
+  %25 = add nsw i32 %24, %23, !dbg !36
+  store i32 %25, ptr %5, align 4, !dbg !36
+  br label %26, !dbg !37
+
+26:                                               ; preds = %22, %19
+    #dbg_declare(ptr %6, !38, !DIExpression(), !39)
+  store i32 7, ptr %6, align 4, !dbg !39
+  %27 = load i32, ptr %6, align 4, !dbg !40
+  call void @fn2(i32 noundef %27), !dbg !41
+    #dbg_declare(ptr %7, !42, !DIExpression(), !43)
+  %28 = load i32, ptr %5, align 4, !dbg !44
+  store i32 %28, ptr %7, align 4, !dbg !43
+  %29 = load i32, ptr %7, align 4, !dbg !45
+  %30 = add nsw i32 %29, 1, !dbg !45
+  store i32 %30, ptr %7, align 4, !dbg !45
+  %31 = load i32, ptr %5, align 4, !dbg !46
+  %32 = add nsw i32 %31, -1, !dbg !46
+  store i32 %32, ptr %5, align 4, !dbg !46
+  %33 = load i32, ptr %5, align 4, !dbg !47
+  call void @fn2(i32 noundef %33), !dbg !48
+  ret void, !dbg !49
+}
+
+declare void @fn2(i32 noundef)
+
+; Function Attrs: noinline nounwind optnone uwtable
+define dso_local i32 @f() !dbg !50 {
+  %1 = alloca i32, align 4
+  %2 = alloca i32, align 4
+    #dbg_declare(ptr %1, !53, !DIExpression(), !54)
+    #dbg_declare(ptr %2, !55, !DIExpression(), !56)
+  call void @fn3(ptr noundef %1), !dbg !57
+  call void @fn3(ptr noundef %2), !dbg !58
+  %3 = load i32, ptr %1, align 4, !dbg !59
+  %4 = load i32, ptr %2, align 4, !dbg !60
+  call void @fn1(i32 noundef %3, i32 noundef %4), !dbg !61
+  ret i32 0, !dbg !62
+}
+
+declare void @fn3(ptr noundef)
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3, !4, !5, !6, !7, !8}
+!llvm.ident = !{!9}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !1, producer: "clang version 19.1.7", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: None)
+!1 = !DIFile(filename: "test.c", directory: "/")
+!2 = !{i32 7, !"Dwarf Version", i32 5}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!4 = !{i32 1, !"wchar_size", i32 4}
+!5 = !{i32 8, !"PIC Level", i32 2}
+!6 = !{i32 7, !"PIE Level", i32 2}
+!7 = !{i32 7, !"uwtable", i32 2}
+!8 = !{i32 7, !"frame-pointer", i32 2}
+!9 = !{!"clang version 19.1.7"}
+!10 = distinct !DISubprogram(name: "fn1", scope: !1, file: !1, line: 3, type: !11, scopeLine: 3, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !14)
+!11 = !DISubroutineType(types: !12)
+!12 = !{null, !13, !13}
+!13 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!14 = !{}
+!15 = !DILocalVariable(name: "x", arg: 1, scope: !10, file: !1, line: 3, type: !13)
+!16 = !DILocation(line: 3, column: 40, scope: !10)
+!17 = !DILocalVariable(name: "y", arg: 2, scope: !10, file: !1, line: 3, type: !13)
+!18 = !DILocation(line: 3, column: 47, scope: !10)
+!19 = !DILocalVariable(name: "u", scope: !10, file: !1, line: 4, type: !13)
+!20 = !DILocation(line: 4, column: 7, scope: !10)
+!21 = !DILocation(line: 4, column: 11, scope: !10)
+!22 = !DILocation(line: 4, column: 15, scope: !10)
+!23 = !DILocation(line: 4, column: 13, scope: !10)
+!24 = !DILocation(line: 5, column: 7, scope: !25)
+!25 = distinct !DILexicalBlock(scope: !10, file: !1, line: 5, column: 7)
+!26 = !DILocation(line: 5, column: 9, scope: !25)
+!27 = !DILocation(line: 5, column: 7, scope: !10)
+!28 = !DILocation(line: 6, column: 7, scope: !25)
+!29 = !DILocation(line: 6, column: 5, scope: !25)
+!30 = !DILocation(line: 8, column: 7, scope: !25)
+!31 = !DILocation(line: 9, column: 7, scope: !32)
+!32 = distinct !DILexicalBlock(scope: !10, file: !1, line: 9, column: 7)
+!33 = !DILocation(line: 9, column: 9, scope: !32)
+!34 = !DILocation(line: 9, column: 7, scope: !10)
+!35 = !DILocation(line: 10, column: 10, scope: !32)
+!36 = !DILocation(line: 10, column: 7, scope: !32)
+!37 = !DILocation(line: 10, column: 5, scope: !32)
+!38 = !DILocalVariable(name: "a", scope: !10, file: !1, line: 11, type: !13)
+!39 = !DILocation(line: 11, column: 7, scope: !10)
+!40 = !DILocation(line: 12, column: 7, scope: !10)
+!41 = !DILocation(line: 12, column: 3, scope: !10)
+!42 = !DILocalVariable(name: "v", scope: !10, file: !1, line: 13, type: !13)
+!43 = !DILocation(line: 13, column: 7, scope: !10)
+!44 = !DILocation(line: 13, column: 11, scope: !10)
+!45 = !DILocation(line: 14, column: 4, scope: !10)
+!46 = !DILocation(line: 15, column: 4, scope: !10)
+!47 = !DILocation(line: 16, column: 7, scope: !10)
+!48 = !DILocation(line: 16, column: 3, scope: !10)
+!49 = !DILocation(line: 17, column: 1, scope: !10)
+!50 = distinct !DISubprogram(name: "f", scope: !1, file: !1, line: 19, type: !51, scopeLine: 19, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !14)
+!51 = !DISubroutineType(types: !52)
+!52 = !{!13}
+!53 = !DILocalVariable(name: "l", scope: !50, file: !1, line: 20, type: !13)
+!54 = !DILocation(line: 20, column: 7, scope: !50)
+!55 = !DILocalVariable(name: "k", scope: !50, file: !1, line: 20, type: !13)
+!56 = !DILocation(line: 20, column: 10, scope: !50)
+!57 = !DILocation(line: 21, column: 3, scope: !50)
+!58 = !DILocation(line: 22, column: 3, scope: !50)
+!59 = !DILocation(line: 23, column: 7, scope: !50)
+!60 = !DILocation(line: 23, column: 10, scope: !50)
+!61 = !DILocation(line: 23, column: 3, scope: !50)
+!62 = !DILocation(line: 24, column: 3, scope: !50)
diff --git a/llvm/test/tools/llvm-dwarfdump/X86/coverage.test b/llvm/test/tools/llvm-dwarfdump/X86/coverage.test
new file mode 100644
index 0000000000000..15762a38ef7bd
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfdump/X86/coverage.test
@@ -0,0 +1,26 @@
+RUN: llc %S/Inputs/coverage.ll -o %t.o -filetype=obj
+RUN: llc %S/Inputs/coverage-opt.ll -o %t-opt.o -filetype=obj
+
+RUN: llvm-dwarfdump --show-variable-coverage %t.o | FileCheck %s
+
+CHECK:      Variable coverage statistics:
+CHECK-NEXT: Function InlChain Variable Decl LinesCovered
+CHECK-NEXT: f k test.c:20 5
+CHECK-NEXT: f l test.c:20 5
+CHECK-NEXT: fn1 a test.c:11 14
+CHECK-NEXT: fn1 u test.c:4 14
+CHECK-NEXT: fn1 v test.c:13 14
+CHECK-NEXT: fn1 x test.c:3 14
+CHECK-NEXT: fn1 y test.c:3 14
+
+RUN: llvm-dwarfdump --show-variable-coverage --combine-inline-variable-instances %t-opt.o | FileCheck %s --check-prefix=COMBINE
+
+COMBINE:      Variable coverage statistics:
+COMBINE-NEXT: Function InstanceCount Variable Decl LinesCovered
+COMBINE-NEXT: f 1 k test.c:20 5
+COMBINE-NEXT: f 1 l test.c:20 5
+COMBINE-NEXT: fn1 1 a test.c:11 5
+COMBINE-NEXT: fn1 1 u test.c:4 1
+COMBINE-NEXT: fn1 1 v test.c:13 0
+COMBINE-NEXT: fn1 1 x test.c:3 7
+COMBINE-NEXT: fn1 1 y test.c:3 7
diff --git a/llvm/tools/llvm-dwarfdump/CMakeLists.txt b/llvm/tools/llvm-dwarfdump/CMakeLists.txt
index 7a0adf32e938c..1ed3f4901c4fc 100644
--- a/llvm/tools/llvm-dwarfdump/CMakeLists.txt
+++ b/llvm/tools/llvm-dwarfdump/CMakeLists.txt
@@ -11,6 +11,7 @@ set(LLVM_LINK_COMPONENTS
   )
 
 add_llvm_tool(llvm-dwarfdump
+  Coverage.cpp
   SectionSizes.cpp
   Statistics.cpp
   llvm-dwarfdump.cpp
diff --git a/llvm/tools/llvm-dwarfdump/Coverage.cpp b/llvm/tools/llvm-dwarfdump/Coverage.cpp
new file mode 100644
index 0000000000000..26708b855be1e
--- /dev/null
+++ b/llvm/tools/llvm-dwarfdump/Coverage.cpp
@@ -0,0 +1,241 @@
+//===-- Coverage.cpp - Debug info coverage metrics ------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm-dwarfdump.h"
+#include "llvm/ADT/SetOperations.h"
+#include "llvm/BinaryFormat/Dwarf.h"
+#include "llvm/DebugInfo/DIContext.h"
+#include "llvm/DebugInfo/DWARF/DWARFAcceleratorTable.h"
+#include "llvm/DebugInfo/DWARF/DWARFCompileUnit.h"
+#include "llvm/DebugInfo/DWARF/DWARFContext.h"
+#include "llvm/IR/CFG.h"
+#include "llvm/IR/DebugInfoMetadata.h"
+#include "llvm/IR/DebugProgramInstruction.h"
+#include "llvm/IR/Instructions.h"
+#include "llvm/IR/Module.h"
+#include "llvm/IRReader/IRReader.h"
+#include "llvm/Object/ObjectFile.h"
+#include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Support/SourceMgr.h"
+
+using namespace llvm;
+using namespace llvm::dwarf;
+using namespace llvm::object;
+
+/// Pair of file index and line number representing a source location.
+typedef std::pair<uint16_t, size_t> SourceLocation;
+
+/// Returns the set of source lines covered by a variable's debug information,
+/// computed by intersecting the variable's location ranges and the containing
+/// scope's address ranges.
+static DenseSet<SourceLocation>
+computeVariableCoverage(DWARFContext &DICtx, DWARFDie VariableDIE,
+                        const DWARFDebugLine::LineTable *const LineTable) {
+  /// Adds source locations to the set that correspond to an address range.
+  auto addLines = [](const DWARFDebugLine::LineTable *LineTable,
+                     DenseSet<SourceLocation> &Lines, DWARFAddressRange Range) {
+    std::vector<uint32_t> Rows;
+    if (LineTable->lookupAddressRange({Range.LowPC, Range.SectionIndex},
+                                      Range.HighPC - Range.LowPC, Rows)) {
+      for (const auto &RowI : Rows) {
+        const auto Row = LineTable->Rows[RowI];
+        // Lookup can return addresses below the LowPC - filter these out.
+        if (Row.Address.Address < Range.LowPC)
+          continue;
+        const auto FileIndex = Row.File;
+
+        const auto Line = Row.Line;
+        if (Line) // Ignore zero lines.
+          Lines.insert({FileIndex, Line});
+      }
+    }
+  };
+
+  // The optionals below will be empty if no address ranges were found, and
+  // present (but containing an empty set) if ranges were found but contained no
+  // source locations, in order to distinguish the two cases.
+
+  auto Locations = VariableDIE.getLocations(DW_AT_location);
+  std::optional<DenseSet<SourceLocation>> Lines;
+  if (Locations) {
+    for (const auto &L : Locations.get()) {
+      if (L.Range) {
+        if (!Lines)
+          Lines = DenseSet<SourceLocation>();
+        addLines(LineTable, *Lines, L.Range.value());
+      }
+    }
+  } else {
+    // If the variable is optimized out and has no DW_AT_location, return an
+    // empty set instead of falling back to the parent scope's address ranges.
+    consumeError(Locations.takeError());
+    return {};
+  }
+
+  // DW_AT_location attribute may contain overly broad address ranges, or none
+  // at all, so we also consider the parent scope's address ranges if present.
+  auto ParentRanges = VariableDIE.getParent().getAddressRanges();
+  std::optional<DenseSet<SourceLocation>> ParentLines;
+  if (ParentRanges) {
+    ParentLines = DenseSet<SourceLocation>();
+    for (const auto &R : ParentRanges.get())
+      addLines(LineTable, *ParentLines, R);
+  } else {
+    consumeError(ParentRanges.takeError());
+  }
+
+  if (!Lines && ParentLines)
+    Lines = ParentLines;
+  else if (ParentLines)
+    llvm::set_intersect(*Lines, *ParentLines);
+
+  return Lines.value_or(DenseSet<SourceLocation>());
+}
+
+static const SmallVector<DWARFDie> getParentSubroutines(DWARFDie DIE) {
+  SmallVector<DWARFDie> Parents;
+  DWARFDie Parent = DIE;
+  do {
+    if (Parent.getTag() == DW_TAG_subprogram) {
+      Parents.push_back(Parent);
+      break;
+    }
+    if (Parent.getTag() == DW_TAG_inlined_subroutine)
+      Parents.push_back(Parent);
+  } while ((Parent = Parent.getParent()));
+  return Parents;
+}
+
+struct VarKey {
+  const char *const SubprogramName;
+  const char *const Name;
+  std::string DeclFile;
+  uint64_t DeclLine;
+
+  bool operator==(const VarKey &Other) const {
+    return DeclLine == Other.DeclLine &&
+           !strcmp(SubprogramName, Other.SubprogramName) &&
+           !strcmp(Name, Other.Name) && !DeclFile.compare(Other.DeclFile);
+  }
+
+  bool operator<(const VarKey &Other) const {
+    int A = strcmp(SubprogramName, Other.SubprogramName);
+    if (A)
+      return A < 0;
+    int B = strcmp(Name, Other.Name);
+    if (B)
+      return B < 0;
+    int C = DeclFile.compare(Other.DeclFile);
+    if (C)
+      return C < 0;
+    return DeclLine < Other.DeclLine;
+  }
+};
+
+struct VarCoverage {
+  SmallVector<DWARFDie> Parents;
+  size_t Cov;
+  size_t Instances;
+};
+
+typedef std::multimap<VarKey, VarCoverage, std::less<>> VarMap;
+
+static std::optional<const VarKey> getVarKey(DWARFDie Die, DWARFDie Parent) {
+  const auto *const DieName = Die.getName(DINameKind::LinkageName);
+  const auto DieFile =
+      Die.getDeclFile(DILineInfoSpecifier::FileLineInfoKind::RelativeFilePath);
+  const auto *const ParentName = Parent.getName(DINameKind::LinkageName);
+  if (!DieName || !ParentName)
+    return std::nullopt;
+  return VarKey{ParentName, DieName, DieFile, Die.getDeclLine()};
+}
+
+static void displayParents(SmallVector<DWARFDie> Parents, raw_ostream &OS) {
+  bool First = true;
+  for (const auto Parent : Parents) {
+    if (auto FormValue = Parent.find(DW_AT_call_file)) {
+      if (auto OptString = FormValue->getAsFile(
+              DILineInfoSpecifier::FileLineInfoKind::RelativeFilePath)) {
+        if (First)
+          First = false;
+        else
+          OS << ", ";
+        OS << *OptString << ":" << toUnsigned(Parent.find(DW_AT_call_line), 0);
+      }
+    }
+  }
+}
+
+static void displayVariableCoverage(const VarKey &Key, const VarCoverage &Var,
+                                    bool CombineInstances, raw_ostream &OS) {
+  WithColor(OS, HighlightColor::String) << Key.SubprogramName;
+  OS << "\t";
+  if (CombineInstances)
+    OS << Var.Instances;
+  else if (Var.Parents.size())
+    // FIXME: This may overflow the terminal if the inlining chain is large.
+    displayParents(Var.Parents, OS);
+  OS << "\t";
+  WithColor(OS, HighlightColor::String) << Key.Name;
+  OS << "\t" << Key.DeclFile << ":" << Key.DeclLine;
+  OS << "\t" << format("%.3g", ((float)Var.Cov / Var.Instances));
+  OS << "\n";
+}
+
+bool dwarfdump::showVariableCoverage(ObjectFile &Obj, DWARFContext &DICtx,
+                                     bool CombineInstances, raw_ostream &OS) {
+  VarMap Vars;
+
+  for (const auto &U : DICtx.info_section_units()) {
+    const auto *const LT = DICtx.getLineTableForUnit(U.get());
+    for (const auto &Entry : U->dies()) {
+      DWARFDie Die = {U.get(), &Entry};
+      if (Die.getTag() != DW_TAG_variable &&
+          Die.getTag() != DW_TAG_formal_parameter)
+        continue;
+
+      const auto Parents = getParentSubroutines(Die);
+      if (!Parents.size())
+        continue;
+      const auto Parent = Parents.front();
+      auto Key = getVarKey(Die, Parent);
+      if (!Key)
+        continue;
+
+      const auto Cov = computeVariableCoverage(DICtx, Die, LT);
+
+      VarCoverage VarCov = {Parents, Cov.size(), 1};
+
+      Vars.insert({*Key, VarCov});
+    }
+  }
+
+  std::pair<VarMap::iterator, VarMap::iterator> Range;
+
+  OS << "\nVariable coverage statistics:\nFunction\t"
+     << (CombineInstances ? "InstanceCount" : "InlChain")
+     << "\tVariable\tDecl\tLinesCovered\n";
+
+  if (CombineInstances) {
+    for (auto FirstVar = Vars.begin(); FirstVar != Vars.end();
+         FirstVar = Range.second) {
+      Range = Vars.equal_range(FirstVar->first);
+      VarCoverage CombinedCov = {{}, 0, 0};
+      for (auto Var = Range.first; Var != Range.second; ++Var) {
+        ++CombinedCov.Instances;
+        CombinedCov.Cov += Var->second.Cov;
+      }
+      displayVariableCoverage(FirstVar->first, CombinedCov, true, OS);
+    }
+  } else {
+    for (auto Var : Vars)
+      displayVariableCoverage(Var.first, Var.second, false, OS);
+  }
+
+  return true;
+}
diff --git a/llvm/tools/llvm-dwarfdump/llvm-dwarfdump.cpp b/llvm/tools/llvm-dwarfdump/llvm-dwarfdump.cpp
index 6f120f93700f6..26778f51e674a 100644
--- a/llvm/tools/llvm-dwarfdump/llvm-dwarfdump.cpp
+++ b/llvm/tools/llvm-dwarfdump/llvm-dwarfdump.cpp
@@ -334,6 +334,16 @@ static opt<bool> Verbose("verbose",
                          cat(DwarfDumpCategory));
 static alias VerboseAlias("v", desc("Alias for --verbose."), aliasopt(Verbose),
                           cat(DwarfDumpCategory), cl::NotHidden);
+static opt<bool>
+    ShowVariableCoverage("show-variable-coverage",
+                         desc("Show per-variable coverage metrics."),
+                         cat(DwarfDumpCategory));
+static opt<bool> CombineInstances(
+    "combine-inline-variable-instances",
+    desc(
+        "Use with --show-variable-coverage to average variable coverage across "
+        "inlined subroutine instances instead of printing them separately."),
+    cat(DwarfDumpCategory));
 static cl::extrahelp
     HelpResponse("\nPass @FILE as argument to read options from FILE.\n");
 } // namespace
@@ -905,7 +915,7 @@ int main(int argc, char **argv) {
     DumpType |= DIDT_UUID;
   if (DumpAll)
     DumpType = DIDT_All;
-  if (DumpType == DIDT_Null) {
+  if (DumpType == DIDT_Null && !ShowVariableCoverage) {
     if (Verbose || Verify)
       DumpType = DIDT_All;
     else
@@ -957,5 +967,14 @@ int main(int argc, char **argv) {
       Success &= handleFile(Object, dumpObjectFile, OutputFile.os());
   }
 
+  if (ShowVariableCoverage) {
+    auto showCoverage = [&](ObjectFile &Obj, DWARFContext &DICtx,
+                            const Twine &Filename, raw_ostream &OS) {
+      return showVariableCoverage(Obj, DICtx, CombineInstances, OS);
+    };
+    for (StringRef Object : Objects)
+      Success &= handleFile(Object, showCoverage, OutputFile.os());
+  }
+
   return Success ? EXIT_SUCCESS : EXIT_FAILURE;
 }
diff --git a/llvm/tools/llvm-dwarfdump/llvm-dwarfdump.h b/llvm/tools/llvm-dwarfdump/llvm-dwarfdump.h
index cf7da56c91f89..ff6d91f12009e 100644
--- a/llvm/tools/llvm-dwarfdump/llvm-dwarfdump.h
+++ b/llvm/tools/llvm-dwarfdump/llvm-dwarfdump.h
@@ -39,6 +39,8 @@ bool collectStatsForObjectFile(object::ObjectFile &Obj, DWARFContext &DICtx,
 bool collectObjectSectionSizes(object::ObjectFile &Obj, DWARFContext &DICtx,
                                const Twine &Filename, raw_ostream &OS);
 
+bool showVariableCoverage(object::ObjectFile &Obj, DWARFContext &DICtx,
+                          bool CombineInstances, raw_ostream &OS);
 } // namespace dwarfdump
 } // namespace llvm
 



More information about the llvm-commits mailing list