Skip to content

Commit 6f946bb

Browse files
committed
Generate KaniMetadata about which functions we skipped
1 parent 6a8dc61 commit 6f946bb

File tree

5 files changed

+188
-64
lines changed

5 files changed

+188
-64
lines changed

kani-compiler/src/codegen_cprover_gotoc/compiler_interface.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,7 @@ impl GotoCodegenResults {
613613
// removes any contracts logic for ReachabilityType::Test or ReachabilityType::PubFns,
614614
// which are the two ReachabilityTypes under which the compiler calls this function.
615615
contracted_functions: vec![],
616+
autoharness_skipped_fns: None,
616617
}
617618
}
618619

kani-compiler/src/kani_middle/attributes.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,17 @@ impl<'tcx> KaniAttributes<'tcx> {
310310
})
311311
}
312312

313+
// Is this a function inserted by Kani instrumentation?
314+
pub fn is_kani_instrumentation(&self) -> bool {
315+
self.fn_marker().is_some() || self.is_contract_generated()
316+
}
317+
318+
// Is this a contract-generated function?
319+
// Note that this function currently always returns false because of https://github.com/model-checking/kani/issues/3921
320+
fn is_contract_generated(&self) -> bool {
321+
self.map.contains_key(&KaniAttributeKind::IsContractGenerated)
322+
}
323+
313324
/// Return a function marker if any.
314325
pub fn fn_marker(&self) -> Option<Symbol> {
315326
self.attribute_value(KaniAttributeKind::FnMarker)

kani-compiler/src/kani_middle/codegen_units.rs

Lines changed: 142 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
//! Today, only stub / contracts can affect the harness codegen. Thus, we group the harnesses
88
//! according to their stub configuration.
99
10-
use crate::args::ReachabilityType;
10+
use crate::args::{Arguments, ReachabilityType};
1111
use crate::kani_middle::attributes::{KaniAttributes, is_proof_harness};
1212
use crate::kani_middle::kani_functions::{KaniIntrinsic, KaniModel};
1313
use crate::kani_middle::metadata::{
@@ -17,14 +17,17 @@ use crate::kani_middle::reachability::filter_crate_items;
1717
use crate::kani_middle::resolve::expect_resolve_fn;
1818
use crate::kani_middle::stubbing::{check_compatibility, harness_stub_map};
1919
use crate::kani_queries::QueryDb;
20-
use kani_metadata::{ArtifactType, AssignsContract, HarnessKind, HarnessMetadata, KaniMetadata};
20+
use kani_metadata::{
21+
ArtifactType, AssignsContract, AutoHarnessSkipReason, AutoHarnessSkippedFns, HarnessKind,
22+
HarnessMetadata, KaniMetadata,
23+
};
2124
use rustc_hir::def_id::DefId;
2225
use rustc_middle::ty::TyCtxt;
2326
use rustc_session::config::OutputType;
2427
use rustc_smir::rustc_internal;
25-
use stable_mir::CrateDef;
2628
use stable_mir::mir::{TerminatorKind, mono::Instance};
2729
use stable_mir::ty::{FnDef, GenericArgKind, GenericArgs, IndexedVal, RigidTy, TyKind};
30+
use stable_mir::{CrateDef, CrateItem};
2831
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
2932
use std::fs::File;
3033
use std::io::BufWriter;
@@ -46,9 +49,10 @@ struct CrateInfo {
4649

4750
/// We group the harnesses that have the same stubs.
4851
pub struct CodegenUnits {
49-
units: Vec<CodegenUnit>,
50-
harness_info: HashMap<Harness, HarnessMetadata>,
52+
autoharness_skipped_fns: Option<AutoHarnessSkippedFns>,
5153
crate_info: CrateInfo,
54+
harness_info: HashMap<Harness, HarnessMetadata>,
55+
units: Vec<CodegenUnit>,
5256
}
5357

5458
#[derive(Clone, Default, Debug)]
@@ -70,7 +74,12 @@ impl CodegenUnits {
7074
let units = group_by_stubs(tcx, &all_harnesses);
7175
validate_units(tcx, &units);
7276
debug!(?units, "CodegenUnits::new");
73-
CodegenUnits { units, harness_info: all_harnesses, crate_info }
77+
CodegenUnits {
78+
units,
79+
harness_info: all_harnesses,
80+
crate_info,
81+
autoharness_skipped_fns: None,
82+
}
7483
}
7584
ReachabilityType::AllFns => {
7685
let mut all_harnesses = get_all_manual_harnesses(tcx, base_filename);
@@ -80,23 +89,16 @@ impl CodegenUnits {
8089
let kani_fns = queries.kani_functions();
8190
let kani_harness_intrinsic =
8291
kani_fns.get(&KaniIntrinsic::AutomaticHarness.into()).unwrap();
83-
let kani_any_inst = kani_fns.get(&KaniModel::Any.into()).unwrap();
84-
85-
let verifiable_fns = filter_crate_items(tcx, |_, instance: Instance| -> bool {
86-
// If the user specified functions to include or exclude, only allow instances matching those filters.
87-
let user_included = if !args.autoharness_included_functions.is_empty() {
88-
fn_list_contains_instance(&instance, &args.autoharness_included_functions)
89-
} else if !args.autoharness_excluded_functions.is_empty() {
90-
!fn_list_contains_instance(&instance, &args.autoharness_excluded_functions)
91-
} else {
92-
true
93-
};
94-
user_included
95-
&& is_eligible_for_automatic_harness(tcx, instance, *kani_any_inst)
96-
});
92+
93+
let (chosen, skipped) = automatic_harness_partition(
94+
tcx,
95+
args,
96+
*kani_fns.get(&KaniModel::Any.into()).unwrap(),
97+
);
98+
9799
let automatic_harnesses = get_all_automatic_harnesses(
98100
tcx,
99-
verifiable_fns,
101+
chosen,
100102
*kani_harness_intrinsic,
101103
base_filename,
102104
);
@@ -111,14 +113,25 @@ impl CodegenUnits {
111113
})
112114
.collect::<Vec<_>>(),
113115
);
114-
all_harnesses.extend(automatic_harnesses);
116+
all_harnesses.extend(automatic_harnesses.clone());
117+
115118
// No need to validate the units again because validation only checks stubs, and we haven't added any stubs.
116119
debug!(?units, "CodegenUnits::new");
117-
CodegenUnits { units, harness_info: all_harnesses, crate_info }
120+
CodegenUnits {
121+
units,
122+
harness_info: all_harnesses,
123+
crate_info,
124+
autoharness_skipped_fns: Some(skipped),
125+
}
118126
}
119127
_ => {
120128
// Leave other reachability type handling as is for now.
121-
CodegenUnits { units: vec![], harness_info: HashMap::default(), crate_info }
129+
CodegenUnits {
130+
units: vec![],
131+
harness_info: HashMap::default(),
132+
crate_info,
133+
autoharness_skipped_fns: None,
134+
}
122135
}
123136
}
124137
}
@@ -163,6 +176,7 @@ impl CodegenUnits {
163176
unsupported_features: vec![],
164177
test_harnesses,
165178
contracted_functions: gen_contracts_metadata(tcx),
179+
autoharness_skipped_fns: self.autoharness_skipped_fns.clone(),
166180
}
167181
}
168182
}
@@ -339,47 +353,112 @@ fn get_all_automatic_harnesses(
339353
.collect::<HashMap<_, _>>()
340354
}
341355

342-
/// Determine whether `instance` is eligible for automatic verification.
343-
fn is_eligible_for_automatic_harness(tcx: TyCtxt, instance: Instance, any_inst: FnDef) -> bool {
344-
// `instance` is ineligble if it is a harness or has an nonexistent/empty body
345-
if is_proof_harness(tcx, instance) || !instance.has_body() {
346-
return false;
347-
}
348-
let body = instance.body().unwrap();
349-
350-
// `instance` is ineligble if it is an associated item of a Kani trait implementation,
351-
// or part of Kani contract instrumentation.
352-
// FIXME -- find a less hardcoded way of checking the former condition (perhaps upstream PR to StableMIR).
353-
if instance.name().contains("kani::Arbitrary")
354-
|| instance.name().contains("kani::Invariant")
355-
|| KaniAttributes::for_instance(tcx, instance)
356-
.fn_marker()
357-
.is_some_and(|m| m.as_str() == "kani_contract_mode")
358-
{
359-
return false;
360-
}
356+
/// Partition every function in the crate into (chosen, skipped), where `chosen` is a vector of the Instances for which we'll generate automatic harnesses,
357+
/// and `skipped` is a map of function names to the reason why we skipped them.
358+
fn automatic_harness_partition(
359+
tcx: TyCtxt,
360+
args: &Arguments,
361+
kani_any_def: FnDef,
362+
) -> (Vec<Instance>, BTreeMap<String, AutoHarnessSkipReason>) {
363+
// If `filter_list` contains `name`, either as an exact match or a substring.
364+
let filter_contains = |name: &str, filter_list: &[String]| -> bool {
365+
filter_list.iter().any(|filter_name| name.contains(filter_name))
366+
};
367+
368+
// If `func` is not eligible for an automatic harness, return the reason why; if it is eligible, return None.
369+
let skip_reason = |fn_item: CrateItem| -> Option<AutoHarnessSkipReason> {
370+
if KaniAttributes::for_def_id(tcx, fn_item.def_id()).is_kani_instrumentation() {
371+
return Some(AutoHarnessSkipReason::KaniImpl);
372+
}
361373

362-
// Each argument of `instance` must implement Arbitrary.
363-
// Note that this function operates on StableMIR `Instance`s, which are eagerly monomorphized,
364-
// so none of these arguments are generic.
365-
body.arg_locals().iter().all(|arg| {
366-
let kani_any_body =
367-
Instance::resolve(any_inst, &GenericArgs(vec![GenericArgKind::Type(arg.ty)]))
368-
.unwrap()
369-
.body()
370-
.unwrap();
371-
if let TerminatorKind::Call { func, .. } = &kani_any_body.blocks[0].terminator.kind {
372-
if let Some((def, args)) = func.ty(body.arg_locals()).unwrap().kind().fn_def() {
373-
return Instance::resolve(def, &args).is_ok();
374+
let instance = match Instance::try_from(fn_item) {
375+
Ok(inst) => inst,
376+
Err(_) => {
377+
return Some(AutoHarnessSkipReason::GenericFn);
374378
}
379+
};
380+
381+
if !instance.has_body() {
382+
return Some(AutoHarnessSkipReason::NoBody);
375383
}
376-
false
377-
})
378-
}
379384

380-
/// Return whether the name of `instance` is included in `fn_list`,
381-
/// either as a substring or an exact match.
382-
fn fn_list_contains_instance(instance: &Instance, fn_list: &[String]) -> bool {
383-
let pretty_name = instance.name();
384-
fn_list.contains(&pretty_name) || fn_list.iter().any(|fn_name| pretty_name.contains(fn_name))
385+
let name = instance.name();
386+
let body = instance.body().unwrap();
387+
388+
if is_proof_harness(tcx, instance)
389+
|| name.contains("kani::Arbitrary")
390+
|| name.contains("kani::Invariant")
391+
{
392+
return Some(AutoHarnessSkipReason::KaniImpl);
393+
}
394+
395+
if (!args.autoharness_included_functions.is_empty()
396+
&& !filter_contains(&name, &args.autoharness_included_functions))
397+
|| (!args.autoharness_excluded_functions.is_empty()
398+
&& filter_contains(&name, &args.autoharness_excluded_functions))
399+
{
400+
return Some(AutoHarnessSkipReason::UserFilter);
401+
}
402+
403+
// Each argument of `instance` must implement Arbitrary.
404+
// Note that we've already filtered out generic functions, so we know that each of these arguments has a concrete type.
405+
let mut problematic_args = vec![];
406+
for (idx, arg) in body.arg_locals().iter().enumerate() {
407+
let kani_any_body =
408+
Instance::resolve(kani_any_def, &GenericArgs(vec![GenericArgKind::Type(arg.ty)]))
409+
.unwrap()
410+
.body()
411+
.unwrap();
412+
413+
let implements_arbitrary = if let TerminatorKind::Call { func, .. } =
414+
&kani_any_body.blocks[0].terminator.kind
415+
{
416+
if let Some((def, args)) = func.ty(body.arg_locals()).unwrap().kind().fn_def() {
417+
Instance::resolve(def, &args).is_ok()
418+
} else {
419+
false
420+
}
421+
} else {
422+
false
423+
};
424+
425+
if !implements_arbitrary {
426+
// Find the name of the argument by referencing var_debug_info.
427+
// Note that enumerate() starts at 0, while StableMIR argument_index starts at 1, hence the idx+1.
428+
let arg_debug_info = body
429+
.var_debug_info
430+
.iter()
431+
.find(|var| {
432+
var.argument_index.is_some_and(|arg_idx| idx + 1 == usize::from(arg_idx))
433+
})
434+
.expect("Arguments should have corresponding var debug info");
435+
problematic_args.push(arg_debug_info.name.to_string())
436+
}
437+
}
438+
if !problematic_args.is_empty() {
439+
return Some(AutoHarnessSkipReason::MissingArbitraryImpl(problematic_args));
440+
}
441+
None
442+
};
443+
444+
let mut chosen = vec![];
445+
let mut skipped = BTreeMap::new();
446+
447+
// FIXME: ideally, this filter would be matches!(item.kind(), ItemKind::Fn), since that would allow us to generate harnesses for top-level closures,
448+
// c.f. https://github.com/model-checking/kani/issues/3832#issuecomment-2701671798.
449+
// Note that filtering closures out here is a UX choice: we could instead call skip_reason() on closures,
450+
// but the limitations in the linked issue would cause the user to be flooded with reports of "skipping" Kani instrumentation functions.
451+
// Instead, we just pretend closures don't exist in our reporting of what we did and did not verify, which has the downside of also ignoring the user's top-level closures, but those are rare.
452+
let crate_fns =
453+
stable_mir::all_local_items().into_iter().filter(|item| item.ty().kind().is_fn());
454+
455+
for func in crate_fns {
456+
if let Some(reason) = skip_reason(func) {
457+
skipped.insert(func.name(), reason);
458+
} else {
459+
chosen.push(Instance::try_from(func).unwrap());
460+
}
461+
}
462+
463+
(chosen, skipped)
385464
}

kani-driver/src/metadata.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ pub fn merge_kani_metadata(files: Vec<KaniMetadata>) -> KaniMetadata {
9797
unsupported_features: vec![],
9898
test_harnesses: vec![],
9999
contracted_functions: vec![],
100+
autoharness_skipped_fns: None,
100101
};
101102
for md in files {
102103
// Note that we're taking ownership of the original vec, and so we can move the data into the new data structure.
@@ -106,6 +107,7 @@ pub fn merge_kani_metadata(files: Vec<KaniMetadata>) -> KaniMetadata {
106107
result.unsupported_features.extend(md.unsupported_features);
107108
result.test_harnesses.extend(md.test_harnesses);
108109
result.contracted_functions.extend(md.contracted_functions);
110+
// We do not handle autoharness metadata here, since this function is not reachable from the autoharness subcommand.
109111
}
110112
result
111113
}

kani_metadata/src/lib.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
extern crate clap;
55

66
use serde::{Deserialize, Serialize};
7-
use std::{collections::HashSet, path::PathBuf};
7+
use std::{
8+
collections::{BTreeMap, HashSet},
9+
path::PathBuf,
10+
};
11+
use strum_macros::{Display, EnumString};
812

913
pub use artifact::ArtifactType;
1014
pub use cbmc_solver::CbmcSolver;
@@ -33,8 +37,35 @@ pub struct KaniMetadata {
3337
pub test_harnesses: Vec<HarnessMetadata>,
3438
/// The functions with contracts in this crate
3539
pub contracted_functions: Vec<ContractedFunction>,
40+
/// Metadata for the `autoharness` subcommand
41+
pub autoharness_skipped_fns: Option<AutoHarnessSkippedFns>,
3642
}
3743

44+
/// Reasons that Kani does not generate an automatic harness for a function.
45+
#[derive(Debug, Clone, Serialize, Deserialize, Display, EnumString)]
46+
pub enum AutoHarnessSkipReason {
47+
/// The function is generic.
48+
#[strum(serialize = "Generic Function")]
49+
GenericFn,
50+
/// A Kani-internal function: already a harness, implementation of a Kani associated item or Kani contract instrumentation functions).
51+
#[strum(serialize = "Kani implementation")]
52+
KaniImpl,
53+
/// At least one of the function's arguments does not implement kani::Arbitrary
54+
/// (The Vec<String> contains the list of argument names that do not implement it)
55+
#[strum(serialize = "Missing Arbitrary implementation for argument(s)")]
56+
MissingArbitraryImpl(Vec<String>),
57+
/// The function does not have a body.
58+
#[strum(serialize = "The function does not have a body")]
59+
NoBody,
60+
/// The function doesn't match the user's provided filters.
61+
#[strum(serialize = "Did not match provided filters")]
62+
UserFilter,
63+
}
64+
65+
/// For the autoharness subcommand: map function names to the reason why we did not generate an automatic harness for that function.
66+
/// We use an ordered map so that when we print the data, it is ordered alphabetically by function name.
67+
pub type AutoHarnessSkippedFns = BTreeMap<String, AutoHarnessSkipReason>;
68+
3869
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
3970
pub struct ContractedFunction {
4071
/// The fully qualified name the user gave to the function (i.e. includes the module path).

0 commit comments

Comments
 (0)