Skip to content

Commit 5a53af1

Browse files
authored
Introduce test reachability mode (rust-lang#1914)
1 parent e1b346d commit 5a53af1

File tree

8 files changed

+141
-37
lines changed

8 files changed

+141
-37
lines changed

kani-compiler/kani_queries/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pub enum ReachabilityType {
2626
None,
2727
/// Start the cross-crate reachability analysis from all public functions in the local crate.
2828
PubFns,
29+
/// Start the cross-crate reachability analysis from all *test* (i.e. `#[test]`) harnesses in the local crate.
30+
Tests,
2931
}
3032

3133
impl Default for ReachabilityType {

kani-compiler/src/codegen_cprover_gotoc/codegen/function.rs

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,38 @@ impl<'tcx> GotocCtx<'tcx> {
284284
}
285285
}
286286

287+
/// Does this `def_id` have `#[rustc_test_marker]`?
288+
pub fn is_test_harness_description(&self, def_id: DefId) -> bool {
289+
let attrs = self.tcx.get_attrs_unchecked(def_id);
290+
291+
self.tcx.sess.contains_name(attrs, rustc_span::symbol::sym::rustc_test_marker)
292+
}
293+
/// Is this the closure inside of a test description const (i.e. macro expanded from a `#[test]`)?
294+
///
295+
/// We're trying to detect the closure (`||`) inside code like:
296+
///
297+
/// ```ignore
298+
/// #[rustc_test_marker]
299+
/// pub const check_2: test::TestDescAndFn = test::TestDescAndFn {
300+
/// desc: ...,
301+
/// testfn: test::StaticTestFn(|| test::assert_test_result(check_2())),
302+
/// };
303+
/// ```
304+
pub fn is_test_harness_closure(&self, def_id: DefId) -> bool {
305+
if !def_id.is_local() {
306+
return false;
307+
}
308+
309+
let local_def_id = def_id.expect_local();
310+
let hir_id = self.tcx.hir().local_def_id_to_hir_id(local_def_id);
311+
312+
// The parent item of the closure appears to reliably be the `const` declaration item.
313+
let parent_id = self.tcx.hir().get_parent_item(hir_id);
314+
let parent_def_id = parent_id.to_def_id();
315+
316+
self.is_test_harness_description(parent_def_id)
317+
}
318+
287319
/// We record test harness information in kani-metadata, just like we record
288320
/// proof harness information. This is used to support e.g. cargo-kani assess.
289321
///
@@ -293,34 +325,16 @@ impl<'tcx> GotocCtx<'tcx> {
293325
/// as it add asserts for tests that return `Result` types.
294326
fn record_test_harness_metadata(&mut self) {
295327
let def_id = self.current_fn().instance().def_id();
296-
if def_id.is_local() {
297-
let local_def_id = def_id.expect_local();
298-
let hir_id = self.tcx.hir().local_def_id_to_hir_id(local_def_id);
299-
300-
// We want to detect the case where we're codegen'ing the closure inside what test "descriptions"
301-
// are macro-expanded to:
302-
//
303-
// #[rustc_test_marker]
304-
// pub const check_2: test::TestDescAndFn = test::TestDescAndFn {
305-
// desc: ...,
306-
// testfn: test::StaticTestFn(|| test::assert_test_result(check_2())),
307-
// };
308-
309-
// The parent item of the closure appears to reliably be the `const` declaration item.
310-
let parent_id = self.tcx.hir().get_parent_item(hir_id);
311-
let attrs = self.tcx.get_attrs_unchecked(parent_id.to_def_id());
312-
313-
if self.tcx.sess.contains_name(attrs, rustc_span::symbol::sym::rustc_test_marker) {
314-
let loc = self.codegen_span(&self.current_fn().mir().span);
315-
self.test_harnesses.push(HarnessMetadata {
316-
pretty_name: self.current_fn().readable_name().to_owned(),
317-
mangled_name: self.current_fn().name(),
318-
original_file: loc.filename().unwrap(),
319-
original_start_line: loc.start_line().unwrap() as usize,
320-
original_end_line: loc.end_line().unwrap() as usize,
321-
unwind_value: None,
322-
})
323-
}
328+
if self.is_test_harness_closure(def_id) {
329+
let loc = self.codegen_span(&self.current_fn().mir().span);
330+
self.test_harnesses.push(HarnessMetadata {
331+
pretty_name: self.current_fn().readable_name().to_owned(),
332+
mangled_name: self.current_fn().name(),
333+
original_file: loc.filename().unwrap(),
334+
original_start_line: loc.start_line().unwrap() as usize,
335+
original_end_line: loc.end_line().unwrap() as usize,
336+
unwind_value: None,
337+
})
324338
}
325339
}
326340

kani-compiler/src/codegen_cprover_gotoc/compiler_interface.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
66
use crate::codegen_cprover_gotoc::GotocCtx;
77
use crate::kani_middle::mir_transform;
8-
use crate::kani_middle::reachability::{collect_reachable_items, filter_crate_items};
8+
use crate::kani_middle::reachability::{
9+
collect_reachable_items, filter_closures_in_const_crate_items, filter_crate_items,
10+
};
911
use bitflags::_core::any::Any;
1012
use cbmc::goto_program::{symtab_transformer, Location};
1113
use cbmc::{InternedString, MachineModel};
@@ -392,6 +394,14 @@ fn collect_codegen_items<'tcx>(gcx: &GotocCtx<'tcx>) -> Vec<MonoItem<'tcx>> {
392394
let harnesses = filter_crate_items(tcx, |_, def_id| gcx.is_proof_harness(def_id));
393395
collect_reachable_items(tcx, &harnesses).into_iter().collect()
394396
}
397+
ReachabilityType::Tests => {
398+
// We're iterating over crate items here, so what we have to codegen is the "test description" containing the
399+
// test closure that we want to execute
400+
let harnesses = filter_closures_in_const_crate_items(tcx, |_, def_id| {
401+
gcx.is_test_harness_description(def_id)
402+
});
403+
collect_reachable_items(tcx, &harnesses).into_iter().collect()
404+
}
395405
ReachabilityType::None => Vec::new(),
396406
ReachabilityType::PubFns => {
397407
let entry_fn = tcx.entry_fn(()).map(|(id, _)| id);

kani-compiler/src/kani_middle/reachability.rs

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use tracing::{debug, debug_span, trace, warn};
1818
use rustc_data_structures::fingerprint::Fingerprint;
1919
use rustc_data_structures::fx::FxHashSet;
2020
use rustc_data_structures::stable_hasher::{HashStable, StableHasher};
21+
use rustc_hir::def_id::DefId;
2122
use rustc_middle::mir::interpret::{AllocId, ConstValue, ErrorHandled, GlobalAlloc, Scalar};
2223
use rustc_middle::mir::mono::MonoItem;
2324
use rustc_middle::mir::visit::Visitor as MirVisitor;
@@ -28,9 +29,8 @@ use rustc_middle::span_bug;
2829
use rustc_middle::ty::adjustment::PointerCast;
2930
use rustc_middle::ty::{
3031
Closure, ClosureKind, ConstKind, Instance, InstanceDef, ParamEnv, Ty, TyCtxt, TyKind,
31-
TypeFoldable, VtblEntry,
32+
TypeFoldable, VtblEntry, WithOptConstParam,
3233
};
33-
use rustc_span::def_id::DefId;
3434

3535
use crate::kani_middle::coercion;
3636

@@ -54,12 +54,11 @@ pub fn collect_reachable_items<'tcx>(
5454
sorted_items
5555
}
5656

57-
/// Collect all items in the crate that matches the given predicate.
57+
/// Collect all (top-level) items in the crate that matches the given predicate.
5858
pub fn filter_crate_items<F>(tcx: TyCtxt, predicate: F) -> Vec<MonoItem>
5959
where
6060
F: FnMut(TyCtxt, DefId) -> bool,
6161
{
62-
// Filter proof harnesses.
6362
let mut filter = predicate;
6463
tcx.hir_crate_items(())
6564
.items()
@@ -70,6 +69,31 @@ where
7069
.collect()
7170
}
7271

72+
/// Use a predicate to find `const` declarations, then extract all closures from those declarations
73+
///
74+
/// Probably only specifically useful with a predicate to find `TestDescAndFn` const declarations from
75+
/// tests and extract the closures from them.
76+
pub fn filter_closures_in_const_crate_items<F>(tcx: TyCtxt, mut predicate: F) -> Vec<MonoItem>
77+
where
78+
F: FnMut(TyCtxt, DefId) -> bool,
79+
{
80+
let mut roots = Vec::new();
81+
for hir_id in tcx.hir_crate_items(()).items() {
82+
let def_id = hir_id.owner_id.def_id.to_def_id();
83+
if predicate(tcx, def_id) {
84+
// The predicate should only ever apply to monomorphic items
85+
let instance = Instance::mono(tcx, def_id);
86+
let body = tcx.instance_mir(InstanceDef::Item(WithOptConstParam::unknown(def_id)));
87+
let mut extrator =
88+
ConstMonoItemExtractor { tcx, body, instance, collected: FxHashSet::default() };
89+
extrator.visit_body(body);
90+
91+
roots.extend(extrator.collected);
92+
}
93+
}
94+
roots
95+
}
96+
7397
struct MonoItemsCollector<'tcx> {
7498
/// The compiler context.
7599
tcx: TyCtxt<'tcx>,
@@ -477,3 +501,50 @@ fn collect_alloc_items<'tcx>(tcx: TyCtxt<'tcx>, alloc_id: AllocId) -> Vec<MonoIt
477501
};
478502
items
479503
}
504+
505+
/// This MIR Visitor is intended for one specific purpose:
506+
/// Find the closure that exist inside a top-level const declaration generated by
507+
/// test declarations. This allows us to treat this closure instance as a root for
508+
/// the reachability analysis.
509+
///
510+
/// Entry into this visitor will be via `visit_body`
511+
struct ConstMonoItemExtractor<'a, 'tcx> {
512+
tcx: TyCtxt<'tcx>,
513+
collected: FxHashSet<MonoItem<'tcx>>,
514+
instance: Instance<'tcx>,
515+
body: &'a Body<'tcx>,
516+
}
517+
518+
impl<'a, 'tcx> MirVisitor<'tcx> for ConstMonoItemExtractor<'a, 'tcx> {
519+
#[allow(clippy::single_match)]
520+
fn visit_rvalue(&mut self, rvalue: &Rvalue<'tcx>, location: Location) {
521+
trace!(rvalue=?*rvalue, "visit_rvalue");
522+
523+
match *rvalue {
524+
Rvalue::Cast(CastKind::Pointer(PointerCast::ClosureFnPointer(_)), ref operand, _) => {
525+
let source_ty = operand.ty(self.body, self.tcx);
526+
let source_ty = self.instance.subst_mir_and_normalize_erasing_regions(
527+
self.tcx,
528+
ParamEnv::reveal_all(),
529+
source_ty,
530+
);
531+
match *source_ty.kind() {
532+
Closure(def_id, substs) => {
533+
let instance = Instance::resolve_closure(
534+
self.tcx,
535+
def_id,
536+
substs,
537+
ClosureKind::FnOnce,
538+
)
539+
.expect("failed to normalize and resolve closure during codegen");
540+
self.collected.insert(MonoItem::Fn(instance.polymorphize(self.tcx)));
541+
}
542+
_ => unreachable!("Unexpected type: {:?}", source_ty),
543+
}
544+
}
545+
_ => { /* not interesting */ }
546+
}
547+
548+
self.super_rvalue(rvalue, location);
549+
}
550+
}

kani-driver/src/assess.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ pub(crate) fn cargokani_assess_main(mut ctx: KaniSession) -> Result<()> {
216216
ctx.args.unwind = Some(1);
217217
ctx.args.tests = true;
218218
ctx.args.output_format = crate::args::OutputFormat::Terse;
219-
ctx.codegen_pub_fns = true;
219+
ctx.codegen_tests = true;
220220
if ctx.args.jobs.is_none() {
221221
// assess will default to fully parallel instead of single-threaded.
222222
// can be overridden with e.g. `cargo kani --enable-unstable -j 8 assess`

kani-driver/src/call_cargo.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ impl KaniSession {
8282
ReachabilityMode::AllPubFns => {
8383
pkg_args.extend(["--".into(), "--reachability=pub_fns".into()]);
8484
}
85+
ReachabilityMode::Tests => {
86+
pkg_args.extend(["--".into(), "--reachability=tests".into()]);
87+
}
8588
}
8689

8790
// Only joing them at the end. All kani flags must come first.

kani-driver/src/call_single_file.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ impl KaniSession {
5353
ReachabilityMode::Legacy => "--reachability=legacy",
5454
ReachabilityMode::ProofHarnesses => "--reachability=harnesses",
5555
ReachabilityMode::AllPubFns => "--reachability=pub_fns",
56+
ReachabilityMode::Tests => "--reachability=tests",
5657
}
5758
.into(),
5859
);

kani-driver/src/session.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub struct KaniSession {
2626
/// Include all publicly-visible symbols in the generated goto binary, not just those reachable from
2727
/// a proof harness. Useful when attempting to verify things that were not annotated with kani
2828
/// proof attributes.
29-
pub codegen_pub_fns: bool,
29+
pub codegen_tests: bool,
3030

3131
/// The location we found the 'kani_rustc' command
3232
pub kani_compiler: PathBuf,
@@ -56,7 +56,7 @@ impl KaniSession {
5656

5757
Ok(KaniSession {
5858
args,
59-
codegen_pub_fns: false,
59+
codegen_tests: false,
6060
kani_compiler: install.kani_compiler()?,
6161
kani_lib_c: install.kani_lib_c()?,
6262
kani_c_stubs: install.kani_c_stubs()?,
@@ -76,7 +76,9 @@ impl KaniSession {
7676
pub fn reachability_mode(&self) -> ReachabilityMode {
7777
if self.args.legacy_linker {
7878
ReachabilityMode::Legacy
79-
} else if self.args.function.is_some() || self.codegen_pub_fns {
79+
} else if self.codegen_tests {
80+
ReachabilityMode::Tests
81+
} else if self.args.function.is_some() {
8082
ReachabilityMode::AllPubFns
8183
} else {
8284
ReachabilityMode::ProofHarnesses
@@ -88,6 +90,7 @@ pub enum ReachabilityMode {
8890
Legacy,
8991
ProofHarnesses,
9092
AllPubFns,
93+
Tests,
9194
}
9295

9396
impl Drop for KaniSession {

0 commit comments

Comments
 (0)