[llvm] [llvm-cov] Preserve line coverage for trailing zero-length regions (PR #227849)

Maxim Zhukov via llvm-commits llvm-commits at lists.llvm.org
Wed Sep 30 14:48:28 PDT 2026


https://github.com/Mephistophiles updated https://github.com/llvm/llvm-project/pull/227849

>From f12ab468d4a9f189e3ebe863b51870967d1a5cdf Mon Sep 17 00:00:00 2001
From: Maxim Zhukov <mussitantesmortem at gmail.com>
Date: Wed, 30 Sep 2026 23:07:31 +0300
Subject: [PATCH] [llvm-cov] Preserve line coverage for trailing zero-length
 regions

A zero-length code region can be last in source order while still lying
inside an active enclosing region. SegmentBuilder unconditionally marks
that trailing region as skipped, causing LineCoverageStats to treat its
line as unmapped despite the enclosing region's coverage.

The unconditional trailing-region check was introduced in 79a1b5ee5ab0
("[Coverage] Build sorted and unique segments", 2017-09-08). The bug is in
coverage segment construction and does not depend on header classification
or on the region originating from a macro.

One way to expose it is a macro defined in a header treated by Clang as a
system header. With system-header coverage disabled, Clang remaps regions
from the macro body to its invocation site, potentially producing a
trailing zero-length code region. This remapping was added in 702a2b627ff4
("[Coverage] Rework !SystemHeadersCoverage (#91446)", 2024-05-20), first
released in LLVM 19. LCOV export can then contain BRDA records for the
invocation line without a corresponding DA record. In the reproducer,
removing the system-header designation restores the DA record; merely
defining the same macro in an ordinary header does not trigger the issue.

Use the enclosing region's count while it remains active, including when
the count is zero. Retain the existing behavior for explicitly skipped
regions and trailing empty regions without an active enclosing region.

Add unit tests for zero and nonzero enclosing counts and explicit skipped
regions, plus an LCOV regression test using Clang-generated mapping for a
system-header macro.
---
 .../ProfileData/Coverage/CoverageMapping.cpp  |   8 ++-
 ...ailing-zero-length-region-nonzero.proftext |  44 ++++++++++++++
 .../trailing-zero-length-region-zero.proftext |  44 ++++++++++++++
 .../trailing-zero-length-region.covmapping    | Bin 0 -> 439 bytes
 .../Inputs/trailing-zero-length-region.cpp    |  31 ++++++++++
 .../llvm-cov/trailing-zero-length-region.test |  22 +++++++
 .../ProfileData/CoverageMappingTest.cpp       |  56 ++++++++++++++++++
 7 files changed, 202 insertions(+), 3 deletions(-)
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/trailing-zero-length-region-nonzero.proftext
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/trailing-zero-length-region-zero.proftext
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/trailing-zero-length-region.covmapping
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/trailing-zero-length-region.cpp
 create mode 100644 llvm/test/tools/llvm-cov/trailing-zero-length-region.test

diff --git a/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp b/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
index cbb048d15bca2..00eea15f84ac9 100644
--- a/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
+++ b/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
@@ -1273,10 +1273,12 @@ class SegmentBuilder {
 
       // Try to emit a segment for the current region.
       if (CurStartLoc == CR.value().endLoc()) {
-        // Avoid making zero-length regions active. If it's the last region,
-        // emit a skipped segment. Otherwise use its predecessor's count.
+        // Avoid making zero-length regions active. Use the enclosing region's
+        // count if one is still active, even for the last region in source
+        // order. Emit a skipped segment for a trailing empty region only if
+        // there is no enclosing region, or if the region is explicitly skipped.
         const bool Skipped =
-            (CR.index() + 1) == Regions.size() ||
+            ((CR.index() + 1) == Regions.size() && ActiveRegions.empty()) ||
             CR.value().Kind == CounterMappingRegion::SkippedRegion;
         startSegment(ActiveRegions.empty() ? CR.value() : *ActiveRegions.back(),
                      CurStartLoc, !GapRegion, Skipped);
diff --git a/llvm/test/tools/llvm-cov/Inputs/trailing-zero-length-region-nonzero.proftext b/llvm/test/tools/llvm-cov/Inputs/trailing-zero-length-region-nonzero.proftext
new file mode 100644
index 0000000000000..121006ce04311
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/trailing-zero-length-region-nonzero.proftext
@@ -0,0 +1,44 @@
+_Z11should_diagv
+# Func Hash:
+24
+# Num Counters:
+1
+# Counter Values:
+1
+
+_Z4diagv
+# Func Hash:
+0
+# Num Counters:
+1
+# Counter Values:
+1
+
+_Z7watcherb
+# Func Hash:
+3149218504315985
+# Num Counters:
+5
+# Counter Values:
+1
+1
+1
+0
+1
+
+_Z9may_throwb
+# Func Hash:
+2696785
+# Num Counters:
+2
+# Counter Values:
+1
+1
+
+main
+# Func Hash:
+1816
+# Num Counters:
+1
+# Counter Values:
+1
diff --git a/llvm/test/tools/llvm-cov/Inputs/trailing-zero-length-region-zero.proftext b/llvm/test/tools/llvm-cov/Inputs/trailing-zero-length-region-zero.proftext
new file mode 100644
index 0000000000000..40190180ac390
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/trailing-zero-length-region-zero.proftext
@@ -0,0 +1,44 @@
+_Z11should_diagv
+# Func Hash:
+24
+# Num Counters:
+1
+# Counter Values:
+0
+
+_Z4diagv
+# Func Hash:
+0
+# Num Counters:
+1
+# Counter Values:
+0
+
+_Z7watcherb
+# Func Hash:
+3149218504315985
+# Num Counters:
+5
+# Counter Values:
+1
+1
+0
+0
+0
+
+_Z9may_throwb
+# Func Hash:
+2696785
+# Num Counters:
+2
+# Counter Values:
+1
+0
+
+main
+# Func Hash:
+1816
+# Num Counters:
+1
+# Counter Values:
+1
diff --git a/llvm/test/tools/llvm-cov/Inputs/trailing-zero-length-region.covmapping b/llvm/test/tools/llvm-cov/Inputs/trailing-zero-length-region.covmapping
new file mode 100644
index 0000000000000000000000000000000000000000..618212dbb5ece76b4b80246e9928b95ea8535a47
GIT binary patch
literal 439
zcmd1FDa%dHFUw_QfB>t#yO=C2DsFZ68=2^wJE4Epv(x{ikB@$GPrp&4p-!=|_G53O
zu1}{YOB4$me{OJk_Ow&=q~uChPX>$qA}StWLrkG&u>ol&Eu)HCkFT6Rsd at UU?>YU+
z`kG#6&YV2seSNK$?y2i%J<odUpS+;!<$M0j`ODsV8Vn4sF8m9C`WjreUD>c+b`vL%
zEdj(JprrQRLj6-gG9x1cBcp%_g9=d0?!ON2nN7h<LF!-tMV%lo13yqAreEoAud&5R
zRUkW1O%tpXp^lM}m6cIQhQXMTL5V?Kft3kFu`*~dtZZm#;9zCYWH5jl{_AS*bF-AG
zx54gWhX^9f0lG^>mYE4CJp1>~XLp(>MLPr8fg-Mxj10Ju+|R_x%Ph#vC?Unh#K_3b
j#KOrW$i%|T$jZPf$N(gm;RGj$1tt^(*+6Us1~3l*aJ*?Q

literal 0
HcmV?d00001

diff --git a/llvm/test/tools/llvm-cov/Inputs/trailing-zero-length-region.cpp b/llvm/test/tools/llvm-cov/Inputs/trailing-zero-length-region.cpp
new file mode 100644
index 0000000000000..6848fbc4fb825
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/trailing-zero-length-region.cpp
@@ -0,0 +1,31 @@
+// Regenerate with Clang 22 or later, with default system-header coverage:
+// clang++ -fprofile-instr-generate -fcoverage-mapping \
+//   -fcoverage-compilation-dir=/llvm-cov trailing-zero-length-region.cpp -o test
+// llvm-cov convert-for-testing test -o trailing-zero-length-region.covmapping
+// LLVM_PROFILE_FILE=zero.profraw ./test
+// LLVM_PROFILE_FILE=nonzero.profraw ./test throw
+// llvm-profdata merge -text zero.profraw -o trailing-zero-length-region-zero.proftext
+// llvm-profdata merge -text nonzero.profraw -o trailing-zero-length-region-nonzero.proftext
+#ifdef SYSTEM_HEADER
+#pragma clang system_header
+#define DIAG_IMPL() do { if (should_diag()) { diag(); } } while (0)
+#define DIAG_ERROR() DIAG_IMPL()
+#else
+#define SYSTEM_HEADER
+#include __FILE__
+bool should_diag() { return true; }
+void diag() {}
+void may_throw(bool value) { if (value) throw 1; }
+void watcher(bool);
+int main(int argc, char **) {
+  watcher(argc > 1);
+  return 0;
+}
+void watcher(bool value) {
+  try {
+    may_throw(value);
+  } catch (int) {
+    DIAG_ERROR();
+  }
+}
+#endif
diff --git a/llvm/test/tools/llvm-cov/trailing-zero-length-region.test b/llvm/test/tools/llvm-cov/trailing-zero-length-region.test
new file mode 100644
index 0000000000000..bc29b5aab9848
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/trailing-zero-length-region.test
@@ -0,0 +1,22 @@
+// A zero-length code region can be the last region in source order while
+// remaining inside a nonempty code region. In particular, Clang remaps
+// macros in system headers to zero-length regions at the macro invocation.
+// A trailing logging macro inside a catch block must retain line coverage,
+// even when the catch block has not been executed.
+
+// RUN: llvm-profdata merge %S/Inputs/trailing-zero-length-region-zero.proftext -o %t.zero.profdata
+// RUN: llvm-cov export --format=lcov %S/Inputs/trailing-zero-length-region.covmapping -instr-profile %t.zero.profdata | FileCheck %s --check-prefix=ZERO
+// RUN: llvm-profdata merge %S/Inputs/trailing-zero-length-region-nonzero.proftext -o %t.nonzero.profdata
+// RUN: llvm-cov export --format=lcov %S/Inputs/trailing-zero-length-region.covmapping -instr-profile %t.nonzero.profdata | FileCheck %s --check-prefix=NONZERO
+
+// ZERO: {{^}}DA:27,1
+// ZERO-NEXT: {{^}}DA:28,0
+// ZERO-NEXT: {{^}}DA:29,0
+// ZERO: {{^}}BRDA:28,0,0,-
+// ZERO-NEXT: {{^}}BRDA:28,0,1,-
+
+// NONZERO: {{^}}DA:27,1
+// NONZERO-NEXT: {{^}}DA:28,1
+// NONZERO-NEXT: {{^}}DA:29,1
+// NONZERO: {{^}}BRDA:28,0,0,1
+// NONZERO-NEXT: {{^}}BRDA:28,0,1,1
diff --git a/llvm/unittests/ProfileData/CoverageMappingTest.cpp b/llvm/unittests/ProfileData/CoverageMappingTest.cpp
index b268aa7cdd057..6de4899070017 100644
--- a/llvm/unittests/ProfileData/CoverageMappingTest.cpp
+++ b/llvm/unittests/ProfileData/CoverageMappingTest.cpp
@@ -676,6 +676,62 @@ TEST_P(CoverageMappingTest, handle_sandwiched_zero_length_region) {
   EXPECT_EQ(CoverageSegment(4, 17, false), Segments[9]);
 }
 
+TEST_P(CoverageMappingTest, handle_trailing_zero_length_region_with_parent) {
+  ProfileWriter.addRecord({"func", 0x1234, {7, 3}}, Err);
+  startFunction("func", 0x1234);
+
+  addCMR(Counter::getCounter(0), "file1", 1, 1, 5, 5);
+  addCMR(Counter::getCounter(1), "file1", 3, 5, 3, 5);
+
+  EXPECT_THAT_ERROR(loadCoverageMapping(), Succeeded());
+  CoverageData Data = LoadedCoverage->getCoverageForFile("file1");
+  std::vector<CoverageSegment> Segments(Data.begin(), Data.end());
+
+  // The final region in source order is empty, but the enclosing region
+  // still covers its location. Use the enclosing count instead of marking
+  // the line as skipped or using the empty region's count.
+  ASSERT_EQ(3U, Segments.size());
+  EXPECT_EQ(CoverageSegment(1, 1, 7, true), Segments[0]);
+  EXPECT_EQ(CoverageSegment(3, 5, 7, true), Segments[1]);
+  EXPECT_EQ(CoverageSegment(5, 5, false), Segments[2]);
+
+  for (const auto &LCS : getLineCoverageStats(Data)) {
+    EXPECT_TRUE(LCS.isMapped());
+    EXPECT_EQ(7U, LCS.getExecutionCount());
+  }
+}
+
+TEST_P(CoverageMappingTest,
+       handle_trailing_zero_length_region_with_zero_parent) {
+  ProfileWriter.addRecord({"func", 0x1234, {0, 3}}, Err);
+  startFunction("func", 0x1234);
+
+  addCMR(Counter::getCounter(0), "file1", 1, 1, 5, 5);
+  addCMR(Counter::getCounter(1), "file1", 3, 5, 3, 5);
+
+  EXPECT_THAT_ERROR(loadCoverageMapping(), Succeeded());
+  CoverageData Data = LoadedCoverage->getCoverageForFile("file1");
+  for (const auto &LCS : getLineCoverageStats(Data)) {
+    EXPECT_TRUE(LCS.isMapped());
+    EXPECT_EQ(0U, LCS.getExecutionCount());
+  }
+}
+
+TEST_P(CoverageMappingTest, handle_trailing_zero_length_skipped_region) {
+  ProfileWriter.addRecord({"func", 0x1234, {7}}, Err);
+  startFunction("func", 0x1234);
+
+  addCMR(Counter::getCounter(0), "file1", 1, 1, 5, 5);
+  addSkipped("file1", 3, 5, 3, 5);
+
+  EXPECT_THAT_ERROR(loadCoverageMapping(), Succeeded());
+  CoverageData Data = LoadedCoverage->getCoverageForFile("file1");
+  for (const auto &LCS : getLineCoverageStats(Data)) {
+    EXPECT_EQ(LCS.getLine() != 3, LCS.isMapped());
+    EXPECT_EQ(LCS.getLine() == 3 ? 0U : 7U, LCS.getExecutionCount());
+  }
+}
+
 TEST_P(CoverageMappingTest, handle_last_completed_region) {
   ProfileWriter.addRecord({"func1", 0x1234, {1, 2, 3, 4}}, Err);
   startFunction("func1", 0x1234);



More information about the llvm-commits mailing list