[llvm] [CoverageMapping] Fix LineCoverageStats incorrectly using gap region count (PR #203732)

Maksim Levental via llvm-commits llvm-commits at lists.llvm.org
Sat Jun 13 16:44:06 PDT 2026


https://github.com/makslevental created https://github.com/llvm/llvm-project/pull/203732

## Summary

Fix a bug in `LineCoverageStats` where lines are incorrectly reported as uncovered (count=0) when:

1. The wrapping segment is a non-gap region with count=0 (e.g., from a never-taken if-branch)
2. The line has no region entry segments (`MinRegionCount == 0`)
3. The line DOES have non-entry, non-gap segments with count > 0 (returning to the parent function's coverage)

This causes `llvm-cov show` and `llvm-cov report` to display count=0 for lines that are genuinely executed, leading to incorrect coverage percentages.

## Example

```cpp
void foo(bool cond) {
  if (cond) {
    int x = 1;   // genuinely uncovered
    (void)x;     // genuinely uncovered
  }              // <-- BUG: reported as uncovered, but this line IS executed
  int y = 2;    // covered
}
```

When `foo(false)` is called, the closing `}` on line 5 is reached (it's part of the parent function's control flow), but `LineCoverageStats` reports it with count=0 because:
- The wrapping segment (from the if-body) has count=0
- No region entries exist on line 5
- The original code returned early with `ExecutionCount = WrappedSegment->Count = 0`

## Fix

Two changes to `LineCoverageStats::LineCoverageStats()`:

1. **Skip gap wrapping segments** when initializing `ExecutionCount` — gap regions should not contribute to a line's execution count.
2. **Check non-entry segments** when `MinRegionCount == 0` — iterate `LineSegments` for any non-gap, `HasCount` segment and use its max count.

## Test plan

- Added `llvm/test/tools/llvm-cov/gap-region-line-coverage.test` (REQUIRES: system-darwin)
- Test inputs generated with Apple clang which produces the specific segment pattern
- Verifies both `show` (line displays count=1) and `report` (line not counted as missed)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

>From 0085eabcf96c95bd11222d8a66c770122d7cc5be Mon Sep 17 00:00:00 2001
From: makslevental <maksim.levental at gmail.com>
Date: Sat, 13 Jun 2026 16:33:04 -0700
Subject: [PATCH 1/2] [llvm-cov] Add failing test for gap region line coverage
 bug

LineCoverageStats incorrectly reports a line as uncovered when the
wrapping segment has count=0 and the line has no region entries, even
if it has non-entry segments with count > 0.

This test uses coverage data from Apple clang which produces the
specific segment pattern that triggers this: a closing brace "}" after
a never-taken if-block, where the non-gap segment returning to the
parent function's count is ignored.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply at anthropic.com>
---
 .../gap-region-quirk/gap-quirk.covmapping     | Bin 0 -> 213 bytes
 .../Inputs/gap-region-quirk/gap-quirk.cpp     |  13 +++++++
 .../gap-region-quirk/gap-quirk.profdata       | Bin 0 -> 712 bytes
 .../llvm-cov/gap-region-line-coverage.test    |  32 ++++++++++++++++++
 4 files changed, 45 insertions(+)
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.covmapping
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.cpp
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.profdata
 create mode 100644 llvm/test/tools/llvm-cov/gap-region-line-coverage.test

diff --git a/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.covmapping b/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.covmapping
new file mode 100644
index 0000000000000000000000000000000000000000..138bb3f29a5d53fb78e0440eeaaf32e4f47424d1
GIT binary patch
literal 213
zcmd1FDa%dHFUw_QfB?Q1%sdS7QO0Tc`ALkqiJ5sePzfNV4P~+cX(kN at N&S-C0{xuK
zyi{GF38_Vi>8ZLoiMdHBiM;8F1-gZ$nMK)p$pr;qUGBSYzkS1X-C7mMo45pGkcv{_
z>B#d-t}_AwD=Q<TC_57)BRd1P0xJ^(kYZ)vVd!mWXy9OF;ALikXkqwuwfDJM%GBGO
SK)wW23}FrfBO at 0NGZO#{H!g_)

literal 0
HcmV?d00001

diff --git a/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.cpp b/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.cpp
new file mode 100644
index 0000000000000..ad98fbb84f1ff
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.cpp
@@ -0,0 +1,13 @@
+void foo(bool cond) {
+  if (cond) {
+    int x = 1;
+    (void)x;
+  }
+  int y = 2;
+  (void)y;
+}
+
+int main() {
+  foo(false);
+  return 0;
+}
diff --git a/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.profdata b/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.profdata
new file mode 100644
index 0000000000000000000000000000000000000000..586f1295ca0fc237c8abd5823915a4e468da19c4
GIT binary patch
literal 712
zcmeyLQ&5zjmf6V5fE_GgLKWYE#y<e%vq6;!KxvpVW+<N#jfQd=VCn?aVIrvXf;N~=
zRB;Dhn8~Q(4<@sq3P5=cQZN^yN-Hd62MfS#fw|{^Iww}~gmqk4#TiU_u!=9(4GCFv
z^FP$_VO4J+&5u<)!VbIm9Y^fqW=`0}?Qw`RG5osP``j#L>TOu~X at K3qz>u4mnI{2d
z!+Zgwp~4ItP=@>N+i%}+UAKmWDFcH6R6IV)I4wUvY2p$H9~Q{y#-JMq<inJ}v~a)}
IFttz`04JCuo&W#<

literal 0
HcmV?d00001

diff --git a/llvm/test/tools/llvm-cov/gap-region-line-coverage.test b/llvm/test/tools/llvm-cov/gap-region-line-coverage.test
new file mode 100644
index 0000000000000..dfbeede7f52be
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/gap-region-line-coverage.test
@@ -0,0 +1,32 @@
+// REQUIRES: system-darwin
+//
+// Tests that LineCoverageStats correctly handles lines where a zero-count
+// non-gap wrapping segment precedes a non-entry segment with count > 0.
+//
+// In this test, line 5 ("}") is the closing brace of an if-block that was
+// never entered. The wrapping segment from the if-body has count=0, but
+// line 5's own segment has count=1 (returning to the parent function's
+// coverage region). LineCoverageStats should report count=1 for line 5.
+//
+// Test inputs generated with Apple clang (-O0) which produces the specific
+// segment pattern needed to trigger this bug.
+
+// RUN: llvm-cov show %S/Inputs/gap-region-quirk/gap-quirk.covmapping \
+// RUN:   -instr-profile %S/Inputs/gap-region-quirk/gap-quirk.profdata \
+// RUN:   -path-equivalence=/tmp/line-coverage-lambda,%S/Inputs/gap-region-quirk \
+// RUN:   2>&1 | FileCheck -check-prefix=SHOW %s
+
+// Line 5 ("}") should show count 1, not 0.
+// SHOW:      5|      1|  }
+
+// RUN: llvm-cov report %S/Inputs/gap-region-quirk/gap-quirk.covmapping \
+// RUN:   -instr-profile %S/Inputs/gap-region-quirk/gap-quirk.profdata \
+// RUN:   -path-equivalence=/tmp/line-coverage-lambda,%S/Inputs/gap-region-quirk \
+// RUN:   --show-branch-summary=false \
+// RUN:   2>&1 | FileCheck -check-prefix=REPORT %s
+
+// 12 mapped lines, 2 genuinely missed (lines 3-4: if-body never entered).
+// Line 5 should NOT be counted as missed.
+// REPORT: TOTAL
+// REPORT-SAME: 12
+// REPORT-SAME: 2

>From 983fd8919f15bc3b065b93a10fc5859bcfb1aee8 Mon Sep 17 00:00:00 2001
From: makslevental <maksim.levental at gmail.com>
Date: Sat, 13 Jun 2026 16:34:35 -0700
Subject: [PATCH 2/2] [CoverageMapping] Fix LineCoverageStats incorrectly using
 gap region count

When a line's wrapping segment is a gap region with count=0, but the
line has non-gap segments with count > 0 (not region entries),
LineCoverageStats incorrectly reports the line as uncovered.

This happens when a zero-count region (e.g., from a never-taken
if-branch) extends past subsequent covered lines via a gap segment.
The fix:
1. Skip initializing ExecutionCount from gap wrapping segments
2. When MinRegionCount is 0, check non-gap HasCount segments on the
   line and use their max count

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply at anthropic.com>
---
 llvm/lib/ProfileData/Coverage/CoverageMapping.cpp | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp b/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
index 0d9a5a6758f06..c6d432c6be20c 100644
--- a/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
+++ b/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
@@ -1602,10 +1602,18 @@ LineCoverageStats::LineCoverageStats(
 
   // Pick the max count from the non-gap, region entry segments and the
   // wrapped count.
-  if (WrappedSegment)
+  if (WrappedSegment && !WrappedSegment->IsGapRegion)
     ExecutionCount = WrappedSegment->Count;
-  if (!MinRegionCount)
+  if (!MinRegionCount) {
+    // If no region entries on this line, check for non-gap segments with
+    // HasCount that aren't region entries (e.g., continuation of a covered
+    // region after a gap). Use their max count instead of the wrapping
+    // gap segment's count.
+    for (const auto *LS : LineSegments)
+      if (!LS->IsGapRegion && LS->HasCount)
+        ExecutionCount = std::max(ExecutionCount, LS->Count);
     return;
+  }
   for (const auto *LS : LineSegments)
     if (isStartOfRegion(LS))
       ExecutionCount = std::max(ExecutionCount, LS->Count);



More information about the llvm-commits mailing list