[llvm] [llvm-cov][CoverageMapping] Fix LineCoverageStats incorrectly using gap region count (PR #203740)

via llvm-commits llvm-commits at lists.llvm.org
Sat Jun 13 23:23:23 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-pgo

Author: Maksim Levental (makslevental)

<details>
<summary>Changes</summary>

## Summary

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

1. The wrapping segment is a **gap region** with count=0
2. The line has no region entry segments (`MinRegionCount == 0`)
3. The line has non-entry, non-gap segments with count > 0

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

## Example

```cpp
bool process(bool err) {
  {
    if (err) {
      return false;
    }
    int fd = 1;
    (void)fd;
  }
  int result = 42;   // <-- BUG: reported as count=0, actually executed
  (void)result;
  return true;
}
```

When `process(false)` is called, line 15 (`int result = 42;`) is executed but reported with count=0. The gap region from the closing `}` of the scoped block wraps forward and suppresses the count.

The extra statement after the `if` block is required to trigger the bug — without it, clang emits a region entry on the line after `}` (`MinRegionCount > 0`) and the normal code path handles it correctly.

## Root cause

In `LineCoverageStats::LineCoverageStats()`, when `MinRegionCount == 0`:

```cpp
if (WrappedSegment)
  ExecutionCount = WrappedSegment->Count;  // gap has count=0
if (!MinRegionCount)
  return;  // returns early with ExecutionCount=0
```

The non-entry segment on the line (with count > 0) is never considered.

## Fix

When `MinRegionCount == 0` AND the wrapping segment is a gap region, iterate `LineSegments` for non-gap, `HasCount` segments and use their max count:

```cpp
if (WrappedSegment)
  ExecutionCount = WrappedSegment->Count;
if (!MinRegionCount) {
  if (WrappedSegment && WrappedSegment->IsGapRegion) {
    for (const auto *LS : LineSegments)
      if (!LS->IsGapRegion && LS->HasCount)
        ExecutionCount = std::max(ExecutionCount, LS->Count);
  }
  return;
}
```

When the wrapping segment is NOT a gap, the existing behavior is preserved — this avoids incorrectly inflating counts from region-end markers of outer regions.

## Test plan

- Added `llvm/test/tools/llvm-cov/gap-region-line-coverage.test` with a minimal reproducer
- Test inputs generated with `clang++ -fprofile-instr-generate -fcoverage-mapping -O0` (both Apple and upstream clang produce the same gap-region segment pattern)
- Verifies both `show` (line displays count=1) and `report` (line not counted as missed)
- All existing `ProfileDataTests/test_line_coverage_iterator` unit tests pass
- All existing llvm-cov lit tests pass

## PR structure

1. **First commit** ([0034a67](https://github.com/llvm/llvm-project/commit/0034a6791064811f7b342283b457468a6d0807e3)): Adds the failing test — [CI should show it failing](https://github.com/llvm/llvm-project/actions/runs/27488053751).
2. **Second commit** ([2045695](https://github.com/llvm/llvm-project/commit/20456954046bfd16e2f46ba32e22d5f06d03cd49)): Adds the fix — [CI should show the test passing](https://github.com/llvm/llvm-project/actions/runs/27488916981).

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


---
Full diff: https://github.com/llvm/llvm-project/pull/203740.diff


5 Files Affected:

- (modified) llvm/lib/ProfileData/Coverage/CoverageMapping.cpp (+12-1) 
- (added) llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk-v2.cpp (+23) 
- (added) llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.covmapping () 
- (added) llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.profdata () 
- (added) llvm/test/tools/llvm-cov/gap-region-line-coverage.test (+37) 


``````````diff
diff --git a/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp b/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
index 0d9a5a6758f06..797ef9c55bfa6 100644
--- a/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
+++ b/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
@@ -1604,8 +1604,19 @@ LineCoverageStats::LineCoverageStats(
   // wrapped count.
   if (WrappedSegment)
     ExecutionCount = WrappedSegment->Count;
-  if (!MinRegionCount)
+  if (!MinRegionCount) {
+    // If no region entries on this line and the wrapping segment is a gap
+    // region, check for non-gap segments with HasCount (e.g., continuation
+    // of a covered region after a gap). Use their max count instead of the
+    // gap's count of 0.
+    if (WrappedSegment && WrappedSegment->IsGapRegion) {
+      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);
diff --git a/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk-v2.cpp b/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk-v2.cpp
new file mode 100644
index 0000000000000..f235382db64e8
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk-v2.cpp
@@ -0,0 +1,23 @@
+// A scoped block with a never-taken early return followed by at least one
+// statement is needed to produce the gap region pattern. Without the extra
+// statement after the if-block, clang emits a region entry on the line after
+// the closing "}" (MinRegionCount > 0) and the bug doesn't trigger. With the
+// extra statement, the closing "}" produces a gap region that wraps forward,
+// and the next line has only a non-entry segment (MinRegionCount == 0).
+bool process(bool err) {
+  {
+    if (err) {
+      return false;
+    }
+    int fd = 1;
+    (void)fd;
+  }
+  int result = 42;
+  (void)result;
+  return true;
+}
+
+int main() {
+  process(false);
+  return 0;
+}
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 0000000000000..b10fa7cf82d73
Binary files /dev/null and b/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.covmapping differ
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 0000000000000..3acc509754fcf
Binary files /dev/null and b/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.profdata differ
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..6f1d48c8b2b27
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/gap-region-line-coverage.test
@@ -0,0 +1,37 @@
+// Tests that LineCoverageStats correctly handles lines where a gap region
+// with count=0 wraps into a line that has a non-entry segment with count > 0.
+//
+// In this test, line 15 ("int result = 42;") follows a scoped block containing
+// a never-taken early return. The gap region from the block's closing "}" wraps
+// to line 15, but line 15 has a non-entry segment with count=1 (the function
+// body continues). LineCoverageStats should report count=1 for line 15.
+//
+// Test inputs generated with (CWD and source must be in /tmp for path-equivalence):
+//   cp Inputs/gap-region-quirk/gap-quirk-v2.cpp /tmp/gap-quirk-v2.cpp
+//   cd /tmp
+//   %clang -fprofile-instr-generate -fcoverage-mapping -O0 \
+//       -mllvm -enable-name-compression=false \
+//       -c -o gap-quirk.o gap-quirk-v2.cpp
+//   llvm-cov convert-for-testing gap-quirk.o -o gap-quirk.covmapping
+//   %clang -fprofile-instr-generate -fcoverage-mapping -O0 \
+//       -o gap-quirk gap-quirk-v2.cpp
+//   LLVM_PROFILE_FILE=gap-quirk.profraw ./gap-quirk
+//   llvm-profdata merge --sparse gap-quirk.profraw -o gap-quirk.profdata
+
+// 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,%S/Inputs/gap-region-quirk \
+// RUN:   2>&1 | FileCheck -check-prefix=SHOW %s
+
+// Line 15 ("int result = 42;") should show count 1, not 0.
+// SHOW:     15|      1|  int result = 42;
+
+// 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,%S/Inputs/gap-region-quirk \
+// RUN:   --show-branch-summary=false \
+// RUN:   2>&1 | FileCheck -check-prefix=REPORT %s
+
+// Line 15 should NOT be counted as missed (16 total lines, 2 missed).
+// REPORT: Filename{{.*}}Regions{{.*}}Missed Regions{{.*}}Cover{{.*}}Functions{{.*}}Missed Functions{{.*}}Executed{{.*}}Lines{{.*}}Missed Lines{{.*}}Cover
+// REPORT: gap-quirk-v2.cpp{{[[:space:]]+}}5{{[[:space:]]+}}1{{[[:space:]]+}}80.00%{{[[:space:]]+}}2{{[[:space:]]+}}0{{[[:space:]]+}}100.00%{{[[:space:]]+}}16{{[[:space:]]+}}2{{[[:space:]]+}}87.50%

``````````

</details>


https://github.com/llvm/llvm-project/pull/203740


More information about the llvm-commits mailing list