Auto merge of #85385 - richkadel:simpler-simplify-with-coverage, r=wesleywiser

Reland - Report coverage `0` of dead blocks

Fixes: #84018

With `-Z instrument-coverage`, coverage reporting of dead blocks
(for example, blocks dropped because a conditional branch is dropped,
based on const evaluation) is now supported.

Note, this PR relands an earlier, reverted PR that failed when compiling
generators. The prior issues with generators has been resolved and a new
test was added to prevent future regressions.

Check out the resulting changes to test coverage of dead blocks in the
test coverage reports in this PR.

r? `@tmandry`
fyi: `@wesleywiser`
This commit is contained in:
bors 2021-06-04 10:18:54 +00:00
commit 289ada5ed4
24 changed files with 216 additions and 65 deletions

View File

@ -28,6 +28,7 @@ pub struct Expression {
/// only whitespace or comments). According to LLVM Code Coverage Mapping documentation, "A count /// only whitespace or comments). According to LLVM Code Coverage Mapping documentation, "A count
/// for a gap area is only used as the line execution count if there are no other regions on a /// for a gap area is only used as the line execution count if there are no other regions on a
/// line." /// line."
#[derive(Debug)]
pub struct FunctionCoverage<'tcx> { pub struct FunctionCoverage<'tcx> {
instance: Instance<'tcx>, instance: Instance<'tcx>,
source_hash: u64, source_hash: u64,
@ -113,6 +114,14 @@ pub fn add_counter_expression(
expression_id, lhs, op, rhs, region expression_id, lhs, op, rhs, region
); );
let expression_index = self.expression_index(u32::from(expression_id)); let expression_index = self.expression_index(u32::from(expression_id));
debug_assert!(
expression_index.as_usize() < self.expressions.len(),
"expression_index {} is out of range for expressions.len() = {}
for {:?}",
expression_index.as_usize(),
self.expressions.len(),
self,
);
if let Some(previous_expression) = self.expressions[expression_index].replace(Expression { if let Some(previous_expression) = self.expressions[expression_index].replace(Expression {
lhs, lhs,
op, op,

View File

@ -47,7 +47,7 @@ fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
// if we applied optimizations, we potentially have some cfg to cleanup to // if we applied optimizations, we potentially have some cfg to cleanup to
// make it easier for further passes // make it easier for further passes
if should_simplify { if should_simplify {
simplify_cfg(body); simplify_cfg(tcx, body);
simplify_locals(body, tcx); simplify_locals(body, tcx);
} }
} }

View File

@ -26,7 +26,7 @@ fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
if has_opts_to_apply { if has_opts_to_apply {
let mut opt_applier = OptApplier { tcx, duplicates }; let mut opt_applier = OptApplier { tcx, duplicates };
opt_applier.visit_body(body); opt_applier.visit_body(body);
simplify_cfg(body); simplify_cfg(tcx, body);
} }
} }
} }

View File

@ -164,7 +164,7 @@ fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
// Since this optimization adds new basic blocks and invalidates others, // Since this optimization adds new basic blocks and invalidates others,
// clean up the cfg to make it nicer for other passes // clean up the cfg to make it nicer for other passes
if should_cleanup { if should_cleanup {
simplify_cfg(body); simplify_cfg(tcx, body);
} }
} }
} }

View File

@ -964,7 +964,7 @@ fn create_generator_drop_shim<'tcx>(
// Make sure we remove dead blocks to remove // Make sure we remove dead blocks to remove
// unrelated code from the resume part of the function // unrelated code from the resume part of the function
simplify::remove_dead_blocks(&mut body); simplify::remove_dead_blocks(tcx, &mut body);
dump_mir(tcx, None, "generator_drop", &0, &body, |_, _| Ok(())); dump_mir(tcx, None, "generator_drop", &0, &body, |_, _| Ok(()));
@ -1137,7 +1137,7 @@ fn create_generator_resume_function<'tcx>(
// Make sure we remove dead blocks to remove // Make sure we remove dead blocks to remove
// unrelated code from the drop part of the function // unrelated code from the drop part of the function
simplify::remove_dead_blocks(body); simplify::remove_dead_blocks(tcx, body);
dump_mir(tcx, None, "generator_resume", &0, body, |_, _| Ok(())); dump_mir(tcx, None, "generator_resume", &0, body, |_, _| Ok(()));
} }

View File

@ -57,7 +57,7 @@ fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
if inline(tcx, body) { if inline(tcx, body) {
debug!("running simplify cfg on {:?}", body.source); debug!("running simplify cfg on {:?}", body.source);
CfgSimplifier::new(body).simplify(); CfgSimplifier::new(body).simplify();
remove_dead_blocks(body); remove_dead_blocks(tcx, body);
} }
} }
} }

View File

@ -167,7 +167,7 @@ fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
} }
if should_cleanup { if should_cleanup {
simplify_cfg(body); simplify_cfg(tcx, body);
} }
} }
} }

View File

@ -38,6 +38,6 @@ fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
} }
} }
simplify::remove_dead_blocks(body) simplify::remove_dead_blocks(tcx, body)
} }
} }

View File

@ -36,7 +36,7 @@ fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
// if we applied optimizations, we potentially have some cfg to cleanup to // if we applied optimizations, we potentially have some cfg to cleanup to
// make it easier for further passes // make it easier for further passes
if should_simplify { if should_simplify {
simplify_cfg(body); simplify_cfg(tcx, body);
} }
} }
} }

View File

@ -29,6 +29,7 @@
use crate::transform::MirPass; use crate::transform::MirPass;
use rustc_index::vec::{Idx, IndexVec}; use rustc_index::vec::{Idx, IndexVec};
use rustc_middle::mir::coverage::*;
use rustc_middle::mir::visit::{MutVisitor, MutatingUseContext, PlaceContext, Visitor}; use rustc_middle::mir::visit::{MutVisitor, MutatingUseContext, PlaceContext, Visitor};
use rustc_middle::mir::*; use rustc_middle::mir::*;
use rustc_middle::ty::TyCtxt; use rustc_middle::ty::TyCtxt;
@ -46,9 +47,9 @@ pub fn new(label: &str) -> Self {
} }
} }
pub fn simplify_cfg(body: &mut Body<'_>) { pub fn simplify_cfg(tcx: TyCtxt<'tcx>, body: &mut Body<'_>) {
CfgSimplifier::new(body).simplify(); CfgSimplifier::new(body).simplify();
remove_dead_blocks(body); remove_dead_blocks(tcx, body);
// FIXME: Should probably be moved into some kind of pass manager // FIXME: Should probably be moved into some kind of pass manager
body.basic_blocks_mut().raw.shrink_to_fit(); body.basic_blocks_mut().raw.shrink_to_fit();
@ -59,9 +60,9 @@ fn name(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.label) Cow::Borrowed(&self.label)
} }
fn run_pass(&self, _tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) { fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
debug!("SimplifyCfg({:?}) - simplifying {:?}", self.label, body.source); debug!("SimplifyCfg({:?}) - simplifying {:?}", self.label, body.source);
simplify_cfg(body); simplify_cfg(tcx, body);
} }
} }
@ -286,7 +287,7 @@ fn strip_nops(&mut self) {
} }
} }
pub fn remove_dead_blocks(body: &mut Body<'_>) { pub fn remove_dead_blocks(tcx: TyCtxt<'tcx>, body: &mut Body<'_>) {
let reachable = traversal::reachable_as_bitset(body); let reachable = traversal::reachable_as_bitset(body);
let num_blocks = body.basic_blocks().len(); let num_blocks = body.basic_blocks().len();
if num_blocks == reachable.count() { if num_blocks == reachable.count() {
@ -306,6 +307,11 @@ pub fn remove_dead_blocks(body: &mut Body<'_>) {
} }
used_blocks += 1; used_blocks += 1;
} }
if tcx.sess.instrument_coverage() {
save_unreachable_coverage(basic_blocks, used_blocks);
}
basic_blocks.raw.truncate(used_blocks); basic_blocks.raw.truncate(used_blocks);
for block in basic_blocks { for block in basic_blocks {
@ -315,6 +321,75 @@ pub fn remove_dead_blocks(body: &mut Body<'_>) {
} }
} }
/// Some MIR transforms can determine at compile time that a sequences of
/// statements will never be executed, so they can be dropped from the MIR.
/// For example, an `if` or `else` block that is guaranteed to never be executed
/// because its condition can be evaluated at compile time, such as by const
/// evaluation: `if false { ... }`.
///
/// Those statements are bypassed by redirecting paths in the CFG around the
/// `dead blocks`; but with `-Z instrument-coverage`, the dead blocks usually
/// include `Coverage` statements representing the Rust source code regions to
/// be counted at runtime. Without these `Coverage` statements, the regions are
/// lost, and the Rust source code will show no coverage information.
///
/// What we want to show in a coverage report is the dead code with coverage
/// counts of `0`. To do this, we need to save the code regions, by injecting
/// `Unreachable` coverage statements. These are non-executable statements whose
/// code regions are still recorded in the coverage map, representing regions
/// with `0` executions.
fn save_unreachable_coverage(
basic_blocks: &mut IndexVec<BasicBlock, BasicBlockData<'_>>,
first_dead_block: usize,
) {
let has_live_counters = basic_blocks.raw[0..first_dead_block].iter().any(|live_block| {
live_block.statements.iter().any(|statement| {
if let StatementKind::Coverage(coverage) = &statement.kind {
matches!(coverage.kind, CoverageKind::Counter { .. })
} else {
false
}
})
});
if !has_live_counters {
// If there are no live `Counter` `Coverage` statements anymore, don't
// move dead coverage to the `START_BLOCK`. Just allow the dead
// `Coverage` statements to be dropped with the dead blocks.
//
// The `generator::StateTransform` MIR pass can create atypical
// conditions, where all live `Counter`s are dropped from the MIR.
//
// At least one Counter per function is required by LLVM (and necessary,
// to add the `function_hash` to the counter's call to the LLVM
// intrinsic `instrprof.increment()`).
return;
}
// Retain coverage info for dead blocks, so coverage reports will still
// report `0` executions for the uncovered code regions.
let mut dropped_coverage = Vec::new();
for dead_block in basic_blocks.raw[first_dead_block..].iter() {
for statement in dead_block.statements.iter() {
if let StatementKind::Coverage(coverage) = &statement.kind {
if let Some(code_region) = &coverage.code_region {
dropped_coverage.push((statement.source_info, code_region.clone()));
}
}
}
}
let start_block = &mut basic_blocks[START_BLOCK];
for (source_info, code_region) in dropped_coverage {
start_block.statements.push(Statement {
source_info,
kind: StatementKind::Coverage(box Coverage {
kind: CoverageKind::Unreachable,
code_region: Some(code_region),
}),
})
}
}
pub struct SimplifyLocals; pub struct SimplifyLocals;
impl<'tcx> MirPass<'tcx> for SimplifyLocals { impl<'tcx> MirPass<'tcx> for SimplifyLocals {

View File

@ -558,7 +558,7 @@ fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
if did_remove_blocks { if did_remove_blocks {
// We have dead blocks now, so remove those. // We have dead blocks now, so remove those.
simplify::remove_dead_blocks(body); simplify::remove_dead_blocks(tcx, body);
} }
} }
} }

View File

@ -60,7 +60,7 @@ fn run_pass<'tcx>(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
} }
if replaced { if replaced {
simplify::remove_dead_blocks(body); simplify::remove_dead_blocks(tcx, body);
} }
} }
} }

View File

@ -12,6 +12,7 @@
12| 1| if b { 12| 1| if b {
13| 1| println!("non_async_func println in block"); 13| 1| println!("non_async_func println in block");
14| 1| } 14| 1| }
^0
15| 1|} 15| 1|}
16| | 16| |
17| | 17| |

View File

@ -5,6 +5,7 @@
5| 1| if true { 5| 1| if true {
6| 1| countdown = 10; 6| 1| countdown = 10;
7| 1| } 7| 1| }
^0
8| | 8| |
9| | const B: u32 = 100; 9| | const B: u32 = 100;
10| 1| let x = if countdown > 7 { 10| 1| let x = if countdown > 7 {
@ -24,6 +25,7 @@
24| 1| if true { 24| 1| if true {
25| 1| countdown = 10; 25| 1| countdown = 10;
26| 1| } 26| 1| }
^0
27| | 27| |
28| 1| if countdown > 7 { 28| 1| if countdown > 7 {
29| 1| countdown -= 4; 29| 1| countdown -= 4;
@ -42,6 +44,7 @@
41| 1| if true { 41| 1| if true {
42| 1| countdown = 10; 42| 1| countdown = 10;
43| 1| } 43| 1| }
^0
44| | 44| |
45| 1| if countdown > 7 { 45| 1| if countdown > 7 {
46| 1| countdown -= 4; 46| 1| countdown -= 4;
@ -54,13 +57,14 @@
53| | } else { 53| | } else {
54| 0| return; 54| 0| return;
55| | } 55| | }
56| | } // Note: closing brace shows uncovered (vs. `0` for implicit else) because condition literal 56| 0| }
57| | // `true` was const-evaluated. The compiler knows the `if` block will be executed. 57| |
58| | 58| |
59| 1| let mut countdown = 0; 59| 1| let mut countdown = 0;
60| 1| if true { 60| 1| if true {
61| 1| countdown = 1; 61| 1| countdown = 1;
62| 1| } 62| 1| }
^0
63| | 63| |
64| 1| let z = if countdown > 7 { 64| 1| let z = if countdown > 7 {
^0 ^0

View File

@ -9,7 +9,7 @@
8| 1|//! assert_eq!(1, 1); 8| 1|//! assert_eq!(1, 1);
9| |//! } else { 9| |//! } else {
10| |//! // this is not! 10| |//! // this is not!
11| |//! assert_eq!(1, 2); 11| 0|//! assert_eq!(1, 2);
12| |//! } 12| |//! }
13| 1|//! ``` 13| 1|//! ```
14| |//! 14| |//!
@ -84,7 +84,7 @@
74| 1| if true { 74| 1| if true {
75| 1| assert_eq!(1, 1); 75| 1| assert_eq!(1, 1);
76| | } else { 76| | } else {
77| | assert_eq!(1, 2); 77| 0| assert_eq!(1, 2);
78| | } 78| | }
79| 1|} 79| 1|}
80| | 80| |

View File

@ -19,11 +19,11 @@
19| 1| if true { 19| 1| if true {
20| 1| println!("Exiting with error..."); 20| 1| println!("Exiting with error...");
21| 1| return Err(1); 21| 1| return Err(1);
22| | } 22| 0| }
23| | 23| 0|
24| | let _ = Firework { strength: 1000 }; 24| 0| let _ = Firework { strength: 1000 };
25| | 25| 0|
26| | Ok(()) 26| 0| Ok(())
27| 1|} 27| 1|}
28| | 28| |
29| |// Expected program output: 29| |// Expected program output:

View File

@ -0,0 +1,32 @@
1| |#![feature(generators, generator_trait)]
2| |
3| |use std::ops::{Generator, GeneratorState};
4| |use std::pin::Pin;
5| |
6| |// The following implementation of a function called from a `yield` statement
7| |// (apparently requiring the Result and the `String` type or constructor)
8| |// creates conditions where the `generator::StateTransform` MIR transform will
9| |// drop all `Counter` `Coverage` statements from a MIR. `simplify.rs` has logic
10| |// to handle this condition, and still report dead block coverage.
11| 1|fn get_u32(val: bool) -> Result<u32, String> {
12| 1| if val { Ok(1) } else { Err(String::from("some error")) }
^0
13| 1|}
14| |
15| 1|fn main() {
16| 1| let is_true = std::env::args().len() == 1;
17| 1| let mut generator = || {
18| 1| yield get_u32(is_true);
19| 1| return "foo";
20| 1| };
21| |
22| 1| match Pin::new(&mut generator).resume(()) {
23| 1| GeneratorState::Yielded(Ok(1)) => {}
24| 0| _ => panic!("unexpected return from resume"),
25| | }
26| 1| match Pin::new(&mut generator).resume(()) {
27| 1| GeneratorState::Complete("foo") => {}
28| 0| _ => panic!("unexpected return from resume"),
29| | }
30| 1|}

View File

@ -52,15 +52,15 @@
30| 1| if true { 30| 1| if true {
31| 1| println!("Exiting with error..."); 31| 1| println!("Exiting with error...");
32| 1| return Err(1); 32| 1| return Err(1);
33| | } // The remaining lines below have no coverage because `if true` (with the constant literal 33| 0| }
34| | // `true`) is guaranteed to execute the `then` block, which is also guaranteed to `return`. 34| 0|
35| | // Thankfully, in the normal case, conditions are not guaranteed ahead of time, and as shown 35| 0|
36| | // in other tests, the lines below would have coverage (which would show they had `0` 36| 0|
37| | // executions, assuming the condition still evaluated to `true`). 37| 0|
38| | 38| 0|
39| | let _ = Firework { strength: 1000 }; 39| 0| let _ = Firework { strength: 1000 };
40| | 40| 0|
41| | Ok(()) 41| 0| Ok(())
42| 1|} 42| 1|}
43| | 43| |
44| |// Expected program output: 44| |// Expected program output:

View File

@ -9,23 +9,23 @@
9| 1| fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 9| 1| fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
10| 1| if true { 10| 1| if true {
11| 1| if false { 11| 1| if false {
12| | while true { 12| 0| while true {
13| | } 13| 0| }
14| 1| } 14| 1| }
15| 1| write!(f, "error")?; 15| 1| write!(f, "cool")?;
^0 ^0
16| | } else { 16| 0| } else {
17| | } 17| 0| }
18| | 18| |
19| 10| for i in 0..10 { 19| 10| for i in 0..10 {
20| 10| if true { 20| 10| if true {
21| 10| if false { 21| 10| if false {
22| | while true {} 22| 0| while true {}
23| 10| } 23| 10| }
24| 10| write!(f, "error")?; 24| 10| write!(f, "cool")?;
^0 ^0
25| | } else { 25| 0| } else {
26| | } 26| 0| }
27| | } 27| | }
28| 1| Ok(()) 28| 1| Ok(())
29| 1| } 29| 1| }
@ -36,21 +36,21 @@
34| |impl std::fmt::Display for DisplayTest { 34| |impl std::fmt::Display for DisplayTest {
35| 1| fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 35| 1| fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
36| 1| if false { 36| 1| if false {
37| | } else { 37| 0| } else {
38| 1| if false { 38| 1| if false {
39| | while true {} 39| 0| while true {}
40| 1| } 40| 1| }
41| 1| write!(f, "error")?; 41| 1| write!(f, "cool")?;
^0 ^0
42| | } 42| | }
43| 10| for i in 0..10 { 43| 10| for i in 0..10 {
44| 10| if false { 44| 10| if false {
45| | } else { 45| 0| } else {
46| 10| if false { 46| 10| if false {
47| | while true {} 47| 0| while true {}
48| 10| } 48| 10| }
49| 10| write!(f, "error")?; 49| 10| write!(f, "cool")?;
^0 ^0
50| | } 50| | }
51| | } 51| | }
52| 1| Ok(()) 52| 1| Ok(())

View File

@ -1,6 +1,6 @@
1| 1|fn main() { 1| 1|fn main() {
2| 1| if false { 2| 1| if false {
3| | loop {} 3| 0| loop {}
4| 1| } 4| 1| }
5| 1|} 5| 1|}

View File

@ -53,8 +53,8 @@ fn main() {
} else { } else {
return; return;
} }
} // Note: closing brace shows uncovered (vs. `0` for implicit else) because condition literal }
// `true` was const-evaluated. The compiler knows the `if` block will be executed.
let mut countdown = 0; let mut countdown = 0;
if true { if true {

View File

@ -0,0 +1,30 @@
#![feature(generators, generator_trait)]
use std::ops::{Generator, GeneratorState};
use std::pin::Pin;
// The following implementation of a function called from a `yield` statement
// (apparently requiring the Result and the `String` type or constructor)
// creates conditions where the `generator::StateTransform` MIR transform will
// drop all `Counter` `Coverage` statements from a MIR. `simplify.rs` has logic
// to handle this condition, and still report dead block coverage.
fn get_u32(val: bool) -> Result<u32, String> {
if val { Ok(1) } else { Err(String::from("some error")) }
}
fn main() {
let is_true = std::env::args().len() == 1;
let mut generator = || {
yield get_u32(is_true);
return "foo";
};
match Pin::new(&mut generator).resume(()) {
GeneratorState::Yielded(Ok(1)) => {}
_ => panic!("unexpected return from resume"),
}
match Pin::new(&mut generator).resume(()) {
GeneratorState::Complete("foo") => {}
_ => panic!("unexpected return from resume"),
}
}

View File

@ -30,11 +30,11 @@ fn main() -> Result<(),u8> {
if true { if true {
println!("Exiting with error..."); println!("Exiting with error...");
return Err(1); return Err(1);
} // The remaining lines below have no coverage because `if true` (with the constant literal }
// `true`) is guaranteed to execute the `then` block, which is also guaranteed to `return`.
// Thankfully, in the normal case, conditions are not guaranteed ahead of time, and as shown
// in other tests, the lines below would have coverage (which would show they had `0`
// executions, assuming the condition still evaluated to `true`).
let _ = Firework { strength: 1000 }; let _ = Firework { strength: 1000 };

View File

@ -12,7 +12,7 @@ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
while true { while true {
} }
} }
write!(f, "error")?; write!(f, "cool")?;
} else { } else {
} }
@ -21,7 +21,7 @@ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if false { if false {
while true {} while true {}
} }
write!(f, "error")?; write!(f, "cool")?;
} else { } else {
} }
} }
@ -38,7 +38,7 @@ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if false { if false {
while true {} while true {}
} }
write!(f, "error")?; write!(f, "cool")?;
} }
for i in 0..10 { for i in 0..10 {
if false { if false {
@ -46,7 +46,7 @@ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if false { if false {
while true {} while true {}
} }
write!(f, "error")?; write!(f, "cool")?;
} }
} }
Ok(()) Ok(())