Skip to content

Commit 0bb7e5c

Browse files
authored
Unrolled build for rust-lang#140847
Rollup merge of rust-lang#140847 - Zalathar:unused-local-file, r=SparrowLii coverage: Detect unused local file IDs to avoid an LLVM assertion Each function's coverage metadata contains a *local file table* that maps local file IDs (used by the function's mapping regions) to global file IDs (shared by all functions in the same CGU). LLVM requires all local file IDs to have at least one mapping region, and has an assertion that will fail if it detects a local file ID with no regions. To make sure that assertion doesn't fire, we need to detect and skip functions whose metadata would trigger it. (This can't actually happen yet, because currently all of a function's spans must belong to the same file and expansion. But this will be an important edge case when adding expansion region support.)
2 parents 7068c8b + 078144f commit 0bb7e5c

16 files changed

+154
-62
lines changed

compiler/rustc_codegen_llvm/src/coverageinfo/ffi.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,20 @@ pub(crate) struct Regions {
155155
impl Regions {
156156
/// Returns true if none of this structure's tables contain any regions.
157157
pub(crate) fn has_no_regions(&self) -> bool {
158+
// Every region has a span, so if there are no spans then there are no regions.
159+
self.all_cov_spans().next().is_none()
160+
}
161+
162+
pub(crate) fn all_cov_spans(&self) -> impl Iterator<Item = &CoverageSpan> {
163+
macro_rules! iter_cov_spans {
164+
( $( $regions:expr ),* $(,)? ) => {
165+
std::iter::empty()
166+
$(
167+
.chain( $regions.iter().map(|region| &region.cov_span) )
168+
)*
169+
}
170+
}
171+
158172
let Self {
159173
code_regions,
160174
expansion_regions,
@@ -163,11 +177,13 @@ impl Regions {
163177
mcdc_decision_regions,
164178
} = self;
165179

166-
code_regions.is_empty()
167-
&& expansion_regions.is_empty()
168-
&& branch_regions.is_empty()
169-
&& mcdc_branch_regions.is_empty()
170-
&& mcdc_decision_regions.is_empty()
180+
iter_cov_spans!(
181+
code_regions,
182+
expansion_regions,
183+
branch_regions,
184+
mcdc_branch_regions,
185+
mcdc_decision_regions,
186+
)
171187
}
172188
}
173189

compiler/rustc_codegen_llvm/src/coverageinfo/mapgen/covfun.rs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use rustc_abi::Align;
1111
use rustc_codegen_ssa::traits::{
1212
BaseTypeCodegenMethods as _, ConstCodegenMethods, StaticCodegenMethods,
1313
};
14+
use rustc_index::IndexVec;
1415
use rustc_middle::mir::coverage::{
1516
BasicCoverageBlock, CovTerm, CoverageIdsInfo, Expression, FunctionCoverageInfo, Mapping,
1617
MappingKind, Op,
@@ -104,6 +105,16 @@ fn fill_region_tables<'tcx>(
104105
ids_info: &'tcx CoverageIdsInfo,
105106
covfun: &mut CovfunRecord<'tcx>,
106107
) {
108+
// If this function is unused, replace all counters with zero.
109+
let counter_for_bcb = |bcb: BasicCoverageBlock| -> ffi::Counter {
110+
let term = if covfun.is_used {
111+
ids_info.term_for_bcb[bcb].expect("every BCB in a mapping was given a term")
112+
} else {
113+
CovTerm::Zero
114+
};
115+
ffi::Counter::from_term(term)
116+
};
117+
107118
// Currently a function's mappings must all be in the same file, so use the
108119
// first mapping's span to determine the file.
109120
let source_map = tcx.sess.source_map();
@@ -115,6 +126,12 @@ fn fill_region_tables<'tcx>(
115126

116127
let local_file_id = covfun.virtual_file_mapping.push_file(&source_file);
117128

129+
// If this testing flag is set, add an extra unused entry to the local
130+
// file table, to help test the code for detecting unused file IDs.
131+
if tcx.sess.coverage_inject_unused_local_file() {
132+
covfun.virtual_file_mapping.push_file(&source_file);
133+
}
134+
118135
// In rare cases, _all_ of a function's spans are discarded, and coverage
119136
// codegen needs to handle that gracefully to avoid #133606.
120137
// It's hard for tests to trigger this organically, so instead we set
@@ -135,16 +152,6 @@ fn fill_region_tables<'tcx>(
135152
// For each counter/region pair in this function+file, convert it to a
136153
// form suitable for FFI.
137154
for &Mapping { ref kind, span } in &fn_cov_info.mappings {
138-
// If this function is unused, replace all counters with zero.
139-
let counter_for_bcb = |bcb: BasicCoverageBlock| -> ffi::Counter {
140-
let term = if covfun.is_used {
141-
ids_info.term_for_bcb[bcb].expect("every BCB in a mapping was given a term")
142-
} else {
143-
CovTerm::Zero
144-
};
145-
ffi::Counter::from_term(term)
146-
};
147-
148155
let Some(coords) = make_coords(span) else { continue };
149156
let cov_span = coords.make_coverage_span(local_file_id);
150157

@@ -177,6 +184,19 @@ fn fill_region_tables<'tcx>(
177184
}
178185
}
179186

187+
/// LLVM requires all local file IDs to have at least one mapping region.
188+
/// If that's not the case, skip this function, to avoid an assertion failure
189+
/// (or worse) in LLVM.
190+
fn check_local_file_table(covfun: &CovfunRecord<'_>) -> bool {
191+
let mut local_file_id_seen =
192+
IndexVec::<u32, _>::from_elem_n(false, covfun.virtual_file_mapping.local_file_table.len());
193+
for cov_span in covfun.regions.all_cov_spans() {
194+
local_file_id_seen[cov_span.file_id] = true;
195+
}
196+
197+
local_file_id_seen.into_iter().all(|seen| seen)
198+
}
199+
180200
/// Generates the contents of the covfun record for this function, which
181201
/// contains the function's coverage mapping data. The record is then stored
182202
/// as a global variable in the `__llvm_covfun` section.
@@ -185,6 +205,10 @@ pub(crate) fn generate_covfun_record<'tcx>(
185205
global_file_table: &GlobalFileTable,
186206
covfun: &CovfunRecord<'tcx>,
187207
) {
208+
if !check_local_file_table(covfun) {
209+
return;
210+
}
211+
188212
let &CovfunRecord {
189213
mangled_function_name,
190214
source_hash,

compiler/rustc_codegen_llvm/src/coverageinfo/mapgen/spans.rs

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ impl Coords {
3939
/// or other expansions), and if it does happen then skipping a span or function is
4040
/// better than an ICE or `llvm-cov` failure that the user might have no way to avoid.
4141
pub(crate) fn make_coords(source_map: &SourceMap, file: &SourceFile, span: Span) -> Option<Coords> {
42-
let span = ensure_non_empty_span(source_map, span)?;
42+
if span.is_empty() {
43+
debug_assert!(false, "can't make coords from empty span: {span:?}");
44+
return None;
45+
}
4346

4447
let lo = span.lo();
4548
let hi = span.hi();
@@ -70,29 +73,6 @@ pub(crate) fn make_coords(source_map: &SourceMap, file: &SourceFile, span: Span)
7073
})
7174
}
7275

73-
fn ensure_non_empty_span(source_map: &SourceMap, span: Span) -> Option<Span> {
74-
if !span.is_empty() {
75-
return Some(span);
76-
}
77-
78-
// The span is empty, so try to enlarge it to cover an adjacent '{' or '}'.
79-
source_map
80-
.span_to_source(span, |src, start, end| try {
81-
// Adjusting span endpoints by `BytePos(1)` is normally a bug,
82-
// but in this case we have specifically checked that the character
83-
// we're skipping over is one of two specific ASCII characters, so
84-
// adjusting by exactly 1 byte is correct.
85-
if src.as_bytes().get(end).copied() == Some(b'{') {
86-
Some(span.with_hi(span.hi() + BytePos(1)))
87-
} else if start > 0 && src.as_bytes()[start - 1] == b'}' {
88-
Some(span.with_lo(span.lo() - BytePos(1)))
89-
} else {
90-
None
91-
}
92-
})
93-
.ok()?
94-
}
95-
9676
/// If `llvm-cov` sees a source region that is improperly ordered (end < start),
9777
/// it will immediately exit with a fatal error. To prevent that from happening,
9878
/// discard regions that are improperly ordered, or might be interpreted in a

compiler/rustc_interface/src/tests.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,8 @@ fn test_unstable_options_tracking_hash() {
776776
CoverageOptions {
777777
level: CoverageLevel::Mcdc,
778778
no_mir_spans: true,
779-
discard_all_spans_in_codegen: true
779+
discard_all_spans_in_codegen: true,
780+
inject_unused_local_file: true,
780781
}
781782
);
782783
tracked!(crate_attr, vec!["abc".to_string()]);

compiler/rustc_mir_transform/src/coverage/spans.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use rustc_data_structures::fx::FxHashSet;
22
use rustc_middle::mir;
33
use rustc_middle::ty::TyCtxt;
4-
use rustc_span::{DesugaringKind, ExpnKind, MacroKind, Span};
4+
use rustc_span::source_map::SourceMap;
5+
use rustc_span::{BytePos, DesugaringKind, ExpnKind, MacroKind, Span};
56
use tracing::instrument;
67

78
use crate::coverage::graph::{BasicCoverageBlock, CoverageGraph};
@@ -83,8 +84,18 @@ pub(super) fn extract_refined_covspans<'tcx>(
8384
// Discard any span that overlaps with a hole.
8485
discard_spans_overlapping_holes(&mut covspans, &holes);
8586

86-
// Perform more refinement steps after holes have been dealt with.
87+
// Discard spans that overlap in unwanted ways.
8788
let mut covspans = remove_unwanted_overlapping_spans(covspans);
89+
90+
// For all empty spans, either enlarge them to be non-empty, or discard them.
91+
let source_map = tcx.sess.source_map();
92+
covspans.retain_mut(|covspan| {
93+
let Some(span) = ensure_non_empty_span(source_map, covspan.span) else { return false };
94+
covspan.span = span;
95+
true
96+
});
97+
98+
// Merge covspans that can be merged.
8899
covspans.dedup_by(|b, a| a.merge_if_eligible(b));
89100

90101
code_mappings.extend(covspans.into_iter().map(|Covspan { span, bcb }| {
@@ -230,3 +241,26 @@ fn compare_spans(a: Span, b: Span) -> std::cmp::Ordering {
230241
// - Both have the same start and span A extends further right
231242
.then_with(|| Ord::cmp(&a.hi(), &b.hi()).reverse())
232243
}
244+
245+
fn ensure_non_empty_span(source_map: &SourceMap, span: Span) -> Option<Span> {
246+
if !span.is_empty() {
247+
return Some(span);
248+
}
249+
250+
// The span is empty, so try to enlarge it to cover an adjacent '{' or '}'.
251+
source_map
252+
.span_to_source(span, |src, start, end| try {
253+
// Adjusting span endpoints by `BytePos(1)` is normally a bug,
254+
// but in this case we have specifically checked that the character
255+
// we're skipping over is one of two specific ASCII characters, so
256+
// adjusting by exactly 1 byte is correct.
257+
if src.as_bytes().get(end).copied() == Some(b'{') {
258+
Some(span.with_hi(span.hi() + BytePos(1)))
259+
} else if start > 0 && src.as_bytes()[start - 1] == b'}' {
260+
Some(span.with_lo(span.lo() - BytePos(1)))
261+
} else {
262+
None
263+
}
264+
})
265+
.ok()?
266+
}

compiler/rustc_session/src/config.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,11 @@ pub struct CoverageOptions {
195195
/// regression tests for #133606, because we don't have an easy way to
196196
/// reproduce it from actual source code.
197197
pub discard_all_spans_in_codegen: bool,
198+
199+
/// `-Zcoverage-options=inject-unused-local-file`: During codegen, add an
200+
/// extra dummy entry to each function's local file table, to exercise the
201+
/// code that checks for local file IDs with no mapping regions.
202+
pub inject_unused_local_file: bool,
198203
}
199204

200205
/// Controls whether branch coverage or MC/DC coverage is enabled.

compiler/rustc_session/src/options.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,6 +1413,7 @@ pub mod parse {
14131413
"mcdc" => slot.level = CoverageLevel::Mcdc,
14141414
"no-mir-spans" => slot.no_mir_spans = true,
14151415
"discard-all-spans-in-codegen" => slot.discard_all_spans_in_codegen = true,
1416+
"inject-unused-local-file" => slot.inject_unused_local_file = true,
14161417
_ => return false,
14171418
}
14181419
}

compiler/rustc_session/src/session.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,11 @@ impl Session {
371371
self.opts.unstable_opts.coverage_options.discard_all_spans_in_codegen
372372
}
373373

374+
/// True if testing flag `-Zcoverage-options=inject-unused-local-file` was passed.
375+
pub fn coverage_inject_unused_local_file(&self) -> bool {
376+
self.opts.unstable_opts.coverage_options.inject_unused_local_file
377+
}
378+
374379
pub fn is_sanitizer_cfi_enabled(&self) -> bool {
375380
self.opts.unstable_opts.sanitizer.contains(SanitizerSet::CFI)
376381
}

tests/coverage/async_closure.cov-map

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,32 +37,29 @@ Number of file 0 mappings: 8
3737
Highest counter ID seen: c0
3838

3939
Function name: async_closure::main::{closure#0}
40-
Raw bytes (14): 0x[01, 01, 00, 02, 01, 0b, 22, 00, 23, 01, 00, 23, 00, 24]
40+
Raw bytes (9): 0x[01, 01, 00, 01, 01, 0b, 22, 00, 24]
4141
Number of files: 1
4242
- file 0 => $DIR/async_closure.rs
4343
Number of expressions: 0
44-
Number of file 0 mappings: 2
45-
- Code(Counter(0)) at (prev + 11, 34) to (start + 0, 35)
46-
- Code(Counter(0)) at (prev + 0, 35) to (start + 0, 36)
44+
Number of file 0 mappings: 1
45+
- Code(Counter(0)) at (prev + 11, 34) to (start + 0, 36)
4746
Highest counter ID seen: c0
4847

4948
Function name: async_closure::main::{closure#0}
50-
Raw bytes (14): 0x[01, 01, 00, 02, 01, 0b, 22, 00, 23, 01, 00, 23, 00, 24]
49+
Raw bytes (9): 0x[01, 01, 00, 01, 01, 0b, 22, 00, 24]
5150
Number of files: 1
5251
- file 0 => $DIR/async_closure.rs
5352
Number of expressions: 0
54-
Number of file 0 mappings: 2
55-
- Code(Counter(0)) at (prev + 11, 34) to (start + 0, 35)
56-
- Code(Counter(0)) at (prev + 0, 35) to (start + 0, 36)
53+
Number of file 0 mappings: 1
54+
- Code(Counter(0)) at (prev + 11, 34) to (start + 0, 36)
5755
Highest counter ID seen: c0
5856

5957
Function name: async_closure::main::{closure#0}::{closure#0}::<i16>
60-
Raw bytes (14): 0x[01, 01, 00, 02, 01, 0b, 22, 00, 23, 01, 00, 23, 00, 24]
58+
Raw bytes (9): 0x[01, 01, 00, 01, 01, 0b, 22, 00, 24]
6159
Number of files: 1
6260
- file 0 => $DIR/async_closure.rs
6361
Number of expressions: 0
64-
Number of file 0 mappings: 2
65-
- Code(Counter(0)) at (prev + 11, 34) to (start + 0, 35)
66-
- Code(Counter(0)) at (prev + 0, 35) to (start + 0, 36)
62+
Number of file 0 mappings: 1
63+
- Code(Counter(0)) at (prev + 11, 34) to (start + 0, 36)
6764
Highest counter ID seen: c0
6865

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
LL| |//@ edition: 2021
2+
LL| |
3+
LL| |// Force this function to be generated in its home crate, so that it ends up
4+
LL| |// with normal coverage metadata.
5+
LL| |#[inline(never)]
6+
LL| 1|pub fn external_function() {}
7+

tests/coverage/unused-local-file.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//! If we give LLVM a local file table for a function, but some of the entries
2+
//! in that table have no associated mapping regions, then an assertion failure
3+
//! will occur in LLVM. We therefore need to detect and skip any function that
4+
//! would trigger that assertion.
5+
//!
6+
//! To test that this case is handled, even before adding code that could allow
7+
//! it to happen organically (for expansion region support), we use a special
8+
//! testing-only flag to force it to occur.
9+
10+
//@ edition: 2024
11+
//@ compile-flags: -Zcoverage-options=inject-unused-local-file
12+
13+
// The `llvm-cov` tool will complain if the test binary ends up having no
14+
// coverage metadata at all. To prevent that, we also link to instrumented
15+
// code in an auxiliary crate that doesn't have the special flag set.
16+
17+
//@ aux-build: discard_all_helper.rs
18+
extern crate discard_all_helper;
19+
20+
fn main() {
21+
discard_all_helper::external_function();
22+
}

tests/mir-opt/coverage/branch_match_arms.main.InstrumentCoverage.diff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
+ coverage Code { bcb: bcb5 } => $DIR/branch_match_arms.rs:19:17: 19:18 (#0);
4141
+ coverage Code { bcb: bcb5 } => $DIR/branch_match_arms.rs:19:23: 19:30 (#0);
4242
+ coverage Code { bcb: bcb5 } => $DIR/branch_match_arms.rs:19:31: 19:32 (#0);
43-
+ coverage Code { bcb: bcb2 } => $DIR/branch_match_arms.rs:21:2: 21:2 (#0);
43+
+ coverage Code { bcb: bcb2 } => $DIR/branch_match_arms.rs:21:1: 21:2 (#0);
4444
+
4545
bb0: {
4646
+ Coverage::VirtualCounter(bcb0);

tests/mir-opt/coverage/instrument_coverage.bar.InstrumentCoverage.diff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
+ coverage Code { bcb: bcb0 } => $DIR/instrument_coverage.rs:27:1: 27:17 (#0);
88
+ coverage Code { bcb: bcb0 } => $DIR/instrument_coverage.rs:28:5: 28:9 (#0);
9-
+ coverage Code { bcb: bcb0 } => $DIR/instrument_coverage.rs:29:2: 29:2 (#0);
9+
+ coverage Code { bcb: bcb0 } => $DIR/instrument_coverage.rs:29:1: 29:2 (#0);
1010
+
1111
bb0: {
1212
+ Coverage::VirtualCounter(bcb0);

tests/mir-opt/coverage/instrument_coverage.main.InstrumentCoverage.diff

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
+ coverage Code { bcb: bcb0 } => $DIR/instrument_coverage.rs:13:1: 13:10 (#0);
1111
+ coverage Code { bcb: bcb1 } => $DIR/instrument_coverage.rs:15:12: 15:15 (#0);
1212
+ coverage Code { bcb: bcb2 } => $DIR/instrument_coverage.rs:16:13: 16:18 (#0);
13-
+ coverage Code { bcb: bcb3 } => $DIR/instrument_coverage.rs:17:10: 17:10 (#0);
14-
+ coverage Code { bcb: bcb2 } => $DIR/instrument_coverage.rs:19:2: 19:2 (#0);
13+
+ coverage Code { bcb: bcb3 } => $DIR/instrument_coverage.rs:17:9: 17:10 (#0);
14+
+ coverage Code { bcb: bcb2 } => $DIR/instrument_coverage.rs:19:1: 19:2 (#0);
1515
+
1616
bb0: {
1717
+ Coverage::VirtualCounter(bcb0);

tests/mir-opt/coverage/instrument_coverage_cleanup.main.CleanupPostBorrowck.diff

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
coverage Code { bcb: bcb0 } => $DIR/instrument_coverage_cleanup.rs:13:1: 13:10 (#0);
1111
coverage Code { bcb: bcb0 } => $DIR/instrument_coverage_cleanup.rs:14:8: 14:36 (#0);
1212
coverage Code { bcb: bcb3 } => $DIR/instrument_coverage_cleanup.rs:14:37: 14:39 (#0);
13-
coverage Code { bcb: bcb1 } => $DIR/instrument_coverage_cleanup.rs:14:39: 14:39 (#0);
14-
coverage Code { bcb: bcb2 } => $DIR/instrument_coverage_cleanup.rs:15:2: 15:2 (#0);
13+
coverage Code { bcb: bcb1 } => $DIR/instrument_coverage_cleanup.rs:14:38: 14:39 (#0);
14+
coverage Code { bcb: bcb2 } => $DIR/instrument_coverage_cleanup.rs:15:1: 15:2 (#0);
1515
coverage Branch { true_bcb: bcb3, false_bcb: bcb1 } => $DIR/instrument_coverage_cleanup.rs:14:8: 14:36 (#0);
1616

1717
bb0: {

tests/mir-opt/coverage/instrument_coverage_cleanup.main.InstrumentCoverage.diff

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
+ coverage Code { bcb: bcb0 } => $DIR/instrument_coverage_cleanup.rs:13:1: 13:10 (#0);
1111
+ coverage Code { bcb: bcb0 } => $DIR/instrument_coverage_cleanup.rs:14:8: 14:36 (#0);
1212
+ coverage Code { bcb: bcb3 } => $DIR/instrument_coverage_cleanup.rs:14:37: 14:39 (#0);
13-
+ coverage Code { bcb: bcb1 } => $DIR/instrument_coverage_cleanup.rs:14:39: 14:39 (#0);
14-
+ coverage Code { bcb: bcb2 } => $DIR/instrument_coverage_cleanup.rs:15:2: 15:2 (#0);
13+
+ coverage Code { bcb: bcb1 } => $DIR/instrument_coverage_cleanup.rs:14:38: 14:39 (#0);
14+
+ coverage Code { bcb: bcb2 } => $DIR/instrument_coverage_cleanup.rs:15:1: 15:2 (#0);
1515
+ coverage Branch { true_bcb: bcb3, false_bcb: bcb1 } => $DIR/instrument_coverage_cleanup.rs:14:8: 14:36 (#0);
1616
+
1717
bb0: {

0 commit comments

Comments
 (0)