Skip to content

Commit 717e99d

Browse files
authored
Autoharness: Update Filtering Options (#4025)
## Summary - fda814b: Make the autoharness filter flags work on the standard library by moving them to `kani_compiler_flags`, which ensures they're passed to all Kani compiler invocations. - c0430ec: Print the crate name in our output tables. - 093fc6b: **Breaking Change** to rename `--include-function` and `--exclude-function` to mention `pattern`s instead, which makes it clearer that they talk about substrings of the total paths (e.g., modules). Also implement the suggestion from #3922 (comment) so that the flags are no longer mutually exclusive. - 9e35fca: Let the above flags filter on crate names as well. - 66444e4: Warn if an exclude flag makes an include flag moot. ## Detail Some more context on why f933799 allows for both flags to be passed now: I realized as part of #3984 how when we call `cargo rustc` for a `cargo kani` invocation, we don't pass `--reachability` to dependencies to avoid running harnesses in them. The problem is that we can't do the same for our cargo command to build the standard library, since that uses `cargo build`, which does not have the same ability to pass flags only to the final compiler invocation and not the dependencies. So we end up passing `--reachability=AllFns` to the dependencies of the standard library as well and generate automatic harnesses for them. If we can pass both filter flags, we can run commands like `kani autoharness --std --include-pattern core --exclude-pattern miniz_oxide`, which will include functions from the `core` crate while excluding functions in the `miniz_oxide` that just happen to have the word "core" in them. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.
1 parent 84cd134 commit 717e99d

File tree

27 files changed

+667
-439
lines changed

27 files changed

+667
-439
lines changed

docs/src/reference/experimental/autoharness.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,46 @@ autoharness feature means that you are also opting into the function contracts a
4343
Kani generates and runs these harnesses internally—the user only sees the verification results.
4444

4545
### Options
46-
The `autoharness` subcommand has options `--include-function` and `--exclude-function` to include and exclude particular functions.
46+
The `autoharness` subcommand has options `--include-pattern` and `--exclude-pattern` to include and exclude particular functions.
4747
These flags look for partial matches against the fully qualified name of a function.
4848

4949
For example, if a module `my_module` has many functions, but we are only interested in `my_module::foo` and `my_module::bar`, we can run:
5050
```
51-
cargo run autoharness -Z autoharness --include-function foo --include-function bar
51+
cargo run autoharness -Z autoharness --include-pattern my_module::foo --include-pattern my_module::bar
5252
```
5353
To exclude `my_module` entirely, run:
5454
```
55-
cargo run autoharness -Z autoharness --exclude-function my_module
55+
cargo run autoharness -Z autoharness --exclude-pattern my_module
5656
```
5757

58+
The selection algorithm is as follows:
59+
- If only `--include-pattern`s are provided, include a function if it matches any of the provided patterns.
60+
- If only `--exclude-pattern`s are provided, include a function if it does not match any of the provided patterns.
61+
- If both are provided, include a function if it matches an include pattern *and* does not match any of the exclude patterns. Note that this implies that the exclude pattern takes precedence, i.e., if a function matches both an include pattern and an exclude pattern, it will be excluded.
62+
63+
Here are some more examples:
64+
65+
```
66+
# Include functions whose paths contain the substring foo or baz, but not foo::bar
67+
kani autoharness -Z autoharness --include-pattern foo --include-pattern baz --exclude-pattern foo::bar
68+
69+
# Include functions whose paths contain the substring foo, but not bar.
70+
kani autoharness -Z autoharness --include-pattern foo --exclude-pattern bar
71+
72+
# Include functions whose paths contain the substring foo::bar, but not bar.
73+
# This ends up including nothing, since all foo::bar matches will also contain bar.
74+
# Kani will emit a warning that these flags conflict.
75+
kani autoharness -Z autoharness --include-pattern foo::bar --exclude-pattern bar
76+
77+
# Include functions whose paths contain the substring foo, but not foo.
78+
# This ends up including nothing, and Kani will emit a warning that these flags conflict.
79+
kani autoharness -Z autoharness --include-pattern foo --exclude--pattern foo
80+
```
81+
82+
Currently, the only supported "patterns" are substrings of the fully qualified path of the function.
83+
However, if more sophisticated patterns (e.g., regular expressions) would be useful for you,
84+
please let us know in a comment on [this GitHub issue](https://github.com/model-checking/kani/issues/3832).
85+
5886
## Example
5987
Using the `estimate_size` example from [First Steps](../../tutorial-first-steps.md) again:
6088
```rust

kani-compiler/src/args.rs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,14 @@ pub struct Arguments {
9191
/// Print the final LLBC file to stdout.
9292
#[clap(long)]
9393
pub print_llbc: bool,
94-
/// If we are running the autoharness subcommand, the functions to include
95-
#[arg(
96-
long = "autoharness-include-function",
97-
num_args(1),
98-
conflicts_with = "autoharness_excluded_functions"
99-
)]
100-
pub autoharness_included_functions: Vec<String>,
101-
/// If we are running the autoharness subcommand, the functions to exclude
102-
#[arg(long = "autoharness-exclude-function", num_args(1))]
103-
pub autoharness_excluded_functions: Vec<String>,
94+
/// If we are running the autoharness subcommand, the paths to include.
95+
/// See kani_driver::autoharness_args for documentation.
96+
#[arg(long = "autoharness-include-pattern", num_args(1))]
97+
pub autoharness_included_patterns: Vec<String>,
98+
/// If we are running the autoharness subcommand, the paths to exclude.
99+
/// See kani_driver::autoharness_args for documentation.
100+
#[arg(long = "autoharness-exclude-pattern", num_args(1))]
101+
pub autoharness_excluded_patterns: Vec<String>,
104102
}
105103

106104
#[derive(Debug, Clone, Copy, AsRefStr, EnumString, VariantNames, PartialEq, Eq)]

kani-compiler/src/kani_middle/codegen_units.rs

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ impl CodegenUnits {
9090
let (chosen, skipped) = automatic_harness_partition(
9191
tcx,
9292
args,
93+
&crate_info.name,
9394
*kani_fns.get(&KaniModel::Any.into()).unwrap(),
9495
);
9596
AUTOHARNESS_MD
@@ -360,6 +361,7 @@ fn get_all_automatic_harnesses(
360361
fn automatic_harness_partition(
361362
tcx: TyCtxt,
362363
args: &Arguments,
364+
crate_name: &str,
363365
kani_any_def: FnDef,
364366
) -> (Vec<Instance>, BTreeMap<String, AutoHarnessSkipReason>) {
365367
// If `filter_list` contains `name`, either as an exact match or a substring.
@@ -384,7 +386,8 @@ fn automatic_harness_partition(
384386
return Some(AutoHarnessSkipReason::NoBody);
385387
}
386388

387-
let name = instance.name();
389+
// Preprend the crate name so that users can filter out entire crates using the existing function filter flags.
390+
let name = format!("{crate_name}::{}", instance.name());
388391
let body = instance.body().unwrap();
389392

390393
if is_proof_harness(tcx, instance)
@@ -394,12 +397,34 @@ fn automatic_harness_partition(
394397
return Some(AutoHarnessSkipReason::KaniImpl);
395398
}
396399

397-
if (!args.autoharness_included_functions.is_empty()
398-
&& !filter_contains(&name, &args.autoharness_included_functions))
399-
|| (!args.autoharness_excluded_functions.is_empty()
400-
&& filter_contains(&name, &args.autoharness_excluded_functions))
401-
{
402-
return Some(AutoHarnessSkipReason::UserFilter);
400+
match (
401+
args.autoharness_included_patterns.is_empty(),
402+
args.autoharness_excluded_patterns.is_empty(),
403+
) {
404+
// If no filters were specified, then continue.
405+
(true, true) => {}
406+
// If only --exclude-pattern was provided, filter out the function if excluded_patterns contains its name.
407+
(true, false) => {
408+
if filter_contains(&name, &args.autoharness_excluded_patterns) {
409+
return Some(AutoHarnessSkipReason::UserFilter);
410+
}
411+
}
412+
// If only --include-pattern was provided, filter out the function if included_patterns does not contain its name.
413+
(false, true) => {
414+
if !filter_contains(&name, &args.autoharness_included_patterns) {
415+
return Some(AutoHarnessSkipReason::UserFilter);
416+
}
417+
}
418+
// If both are specified, filter out the function if included_patterns does not contain its name.
419+
// Then, filter out any functions that excluded_patterns does match.
420+
// This order is important, since it preserves the semantics described in kani_driver::autoharness_args where exclude takes precedence over include.
421+
(false, false) => {
422+
if !filter_contains(&name, &args.autoharness_included_patterns)
423+
|| filter_contains(&name, &args.autoharness_excluded_patterns)
424+
{
425+
return Some(AutoHarnessSkipReason::UserFilter);
426+
}
427+
}
403428
}
404429

405430
// Each argument of `instance` must implement Arbitrary.

kani-driver/src/args/autoharness_args.rs

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,28 @@ use crate::args::{ValidateArgs, VerificationArgs, validate_std_path};
99
use clap::{Error, Parser, error::ErrorKind};
1010
use kani_metadata::UnstableFeature;
1111

12+
// TODO: It would be nice if we could borrow --exact here from VerificationArgs to differentiate between partial/exact matches,
13+
// like --harnesses does. Sharing arguments with VerificationArgs doesn't work with our current structure, though.
1214
#[derive(Debug, Parser)]
1315
pub struct CommonAutoharnessArgs {
14-
/// If specified, only autoharness functions that match this filter. This option can be provided
15-
/// multiple times, which will verify all functions matching any of the filters.
16-
/// Note that this filter will match against partial names, i.e., providing the name of a module will include all functions from that module.
17-
/// Also note that if the function specified is unable to be automatically verified, this flag will have no effect.
18-
#[arg(
19-
long = "include-function",
20-
num_args(1),
21-
value_name = "FUNCTION",
22-
conflicts_with = "exclude_function"
23-
)]
24-
pub include_function: Vec<String>,
25-
26-
/// If specified, only autoharness functions that do not match this filter. This option can be provided
27-
/// multiple times, which will verify all functions that do not match any of the filters.
28-
/// Note that this filter will match against partial names, i.e., providing the name of a module will exclude all functions from that module.
29-
#[arg(long = "exclude-function", num_args(1), value_name = "FUNCTION")]
30-
pub exclude_function: Vec<String>,
31-
// TODO: It would be nice if we could borrow --exact here from VerificationArgs to differentiate between partial/exact matches,
32-
// like --harnesses does. Sharing arguments with VerificationArgs doesn't work with our current structure, though.
16+
/// Only create automatic harnesses for functions that match the given pattern.
17+
/// This option can be provided multiple times, which will verify functions matching any of the patterns.
18+
/// Kani considers a function to match the pattern if its fully qualified path contains PATTERN as a substring.
19+
/// Example: `--include-pattern foo` matches all functions whose fully qualified paths contain the substring "foo".
20+
#[arg(long = "include-pattern", num_args(1), value_name = "PATTERN")]
21+
pub include_pattern: Vec<String>,
22+
23+
/// Only create automatic harnesses for functions that do not match the given pattern.
24+
/// This option can be provided multiple times, which will verify functions that do not match any of the patterns.
25+
/// Kani considers a function to match the pattern if its fully qualified path contains PATTERN as a substring.
26+
27+
/// This option takes precedence over `--include-pattern`, i.e., Kani will first select all functions that match `--include-pattern`,
28+
/// then exclude those that match `--exclude-pattern.`
29+
/// Example: `--include-pattern foo --exclude-pattern foo::bar` creates automatic harnesses for all functions whose paths contain "foo" without "foo::bar".
30+
/// Example: `--include-pattern foo::bar --exclude-pattern foo` makes the `--include-pattern` a no-op, since the exclude pattern is a superset of the include pattern.
31+
#[arg(long = "exclude-pattern", num_args(1), value_name = "PATTERN")]
32+
pub exclude_pattern: Vec<String>,
33+
3334
/// Run the `list` subcommand after generating the automatic harnesses. Requires -Z list. Note that this option implies --only-codegen.
3435
#[arg(long)]
3536
pub list: bool,

kani-driver/src/args/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ pub enum CargoKaniSubcommand {
178178
List(Box<list_args::CargoListArgs>),
179179

180180
/// Create and run harnesses automatically for eligible functions. Implies -Z function-contracts and -Z loop-contracts.
181+
/// See https://model-checking.github.io/kani/reference/experimental/autoharness.html for documentation.
181182
Autoharness(Box<autoharness_args::CargoAutoharnessArgs>),
182183
}
183184

kani-driver/src/autoharness/mod.rs

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ use crate::args::autoharness_args::{
99
};
1010
use crate::args::common::UnstableFeature;
1111
use crate::call_cbmc::VerificationStatus;
12-
use crate::call_single_file::to_rustc_arg;
1312
use crate::harness_runner::HarnessResult;
1413
use crate::list::collect_metadata::process_metadata;
1514
use crate::list::output::output_list_results;
1615
use crate::project::{Project, standalone_project, std_project};
1716
use crate::session::KaniSession;
17+
use crate::util::warning;
1818
use crate::{InvocationType, print_kani_version, project, verify_project};
1919
use anyhow::Result;
2020
use comfy_table::Table as PrettyTable;
@@ -56,8 +56,8 @@ fn setup_session(session: &mut KaniSession, common_autoharness_args: &CommonAuto
5656
session.enable_autoharness();
5757
session.add_default_bounds();
5858
session.add_auto_harness_args(
59-
&common_autoharness_args.include_function,
60-
&common_autoharness_args.exclude_function,
59+
&common_autoharness_args.include_pattern,
60+
&common_autoharness_args.exclude_pattern,
6161
);
6262
}
6363

@@ -84,17 +84,20 @@ fn postprocess_project(
8484
/// Print automatic harness metadata to the terminal.
8585
fn print_autoharness_metadata(metadata: Vec<KaniMetadata>) {
8686
let mut chosen_table = PrettyTable::new();
87-
chosen_table.set_header(vec!["Selected Function"]);
87+
chosen_table.set_header(vec!["Crate", "Selected Function"]);
8888

8989
let mut skipped_table = PrettyTable::new();
90-
skipped_table.set_header(vec!["Skipped Function", "Reason for Skipping"]);
90+
skipped_table.set_header(vec!["Crate", "Skipped Function", "Reason for Skipping"]);
9191

9292
for md in metadata {
9393
let autoharness_md = md.autoharness_md.unwrap();
94-
chosen_table.add_rows(autoharness_md.chosen.into_iter().map(|func| vec![func]));
94+
chosen_table.add_rows(
95+
autoharness_md.chosen.into_iter().map(|func| vec![md.crate_name.clone(), func]),
96+
);
9597
skipped_table.add_rows(autoharness_md.skipped.into_iter().filter_map(|(func, reason)| {
9698
match reason {
9799
AutoHarnessSkipReason::MissingArbitraryImpl(ref args) => Some(vec![
100+
md.crate_name.clone(),
98101
func,
99102
format!(
100103
"{reason} {}",
@@ -106,7 +109,9 @@ fn print_autoharness_metadata(metadata: Vec<KaniMetadata>) {
106109
]),
107110
AutoHarnessSkipReason::GenericFn
108111
| AutoHarnessSkipReason::NoBody
109-
| AutoHarnessSkipReason::UserFilter => Some(vec![func, reason.to_string()]),
112+
| AutoHarnessSkipReason::UserFilter => {
113+
Some(vec![md.crate_name.clone(), func, reason.to_string()])
114+
}
110115
// We don't report Kani implementations to the user to avoid exposing Kani functions we insert during instrumentation.
111116
// For those we don't insert during instrumentation that are in this category (manual harnesses or Kani trait implementations),
112117
// it should be obvious that we wouldn't generate harnesses, so reporting those functions as "skipped" is unlikely to be useful.
@@ -151,23 +156,36 @@ fn print_skipped_table(table: &mut PrettyTable) {
151156
impl KaniSession {
152157
/// Enable autoharness mode.
153158
pub fn enable_autoharness(&mut self) {
154-
self.auto_harness = true;
159+
self.autoharness_compiler_flags = Some(vec![]);
155160
self.args.common_args.unstable_features.enable_feature(UnstableFeature::FunctionContracts);
156161
self.args.common_args.unstable_features.enable_feature(UnstableFeature::LoopContracts);
157162
}
158163

159164
/// Add the compiler arguments specific to the `autoharness` subcommand.
160-
/// TODO: this should really be appending onto the `kani_compiler_flags()` output instead of `pkg_args`.
161-
/// It doesn't affect functionality since autoharness doesn't examine dependencies, but would still be better practice.
162165
pub fn add_auto_harness_args(&mut self, included: &[String], excluded: &[String]) {
163-
for func in included {
164-
self.pkg_args
165-
.push(to_rustc_arg(vec![format!("--autoharness-include-function {}", func)]));
166+
for include_pattern in included {
167+
for exclude_pattern in excluded {
168+
// Check if include pattern contains exclude pattern
169+
// This catches cases like include="foo::bar" exclude="bar" or include="foo" exclude="foo"
170+
if include_pattern.contains(exclude_pattern) {
171+
warning(&format!(
172+
"Include pattern '{}' contains exclude pattern '{}'. \
173+
This combination will never match any functions since all functions matching \
174+
the include pattern will also match the exclude pattern, and the exclude pattern takes precedence.",
175+
include_pattern, exclude_pattern
176+
));
177+
}
178+
}
179+
}
180+
181+
let mut args = vec![];
182+
for pattern in included {
183+
args.push(format!("--autoharness-include-pattern {}", pattern));
166184
}
167-
for func in excluded {
168-
self.pkg_args
169-
.push(to_rustc_arg(vec![format!("--autoharness-exclude-function {}", func)]));
185+
for pattern in excluded {
186+
args.push(format!("--autoharness-exclude-pattern {}", pattern));
170187
}
188+
self.autoharness_compiler_flags = Some(args);
171189
}
172190

173191
/// Add global harness timeout and loop unwinding bounds if not provided.
@@ -196,13 +214,15 @@ impl KaniSession {
196214

197215
let mut verified_fns = PrettyTable::new();
198216
verified_fns.set_header(vec![
217+
"Crate",
199218
"Selected Function",
200219
"Kind of Automatic Harness",
201220
"Verification Result",
202221
]);
203222

204223
for success in successes {
205224
verified_fns.add_row(vec![
225+
success.harness.crate_name.clone(),
206226
success.harness.pretty_name.clone(),
207227
success.harness.attributes.kind.to_string(),
208228
success.result.status.to_string(),
@@ -211,13 +231,16 @@ impl KaniSession {
211231

212232
for failure in failures {
213233
verified_fns.add_row(vec![
234+
failure.harness.crate_name.clone(),
214235
failure.harness.pretty_name.clone(),
215236
failure.harness.attributes.kind.to_string(),
216237
failure.result.status.to_string(),
217238
]);
218239
}
219240

220-
println!("{verified_fns}");
241+
if total > 0 {
242+
println!("{verified_fns}");
243+
}
221244

222245
if failing > 0 {
223246
println!(

kani-driver/src/call_cargo.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ crate-type = ["lib"]
194194
// If you are adding a new `kani-compiler` argument, you likely want to put it `kani_compiler_flags()` instead,
195195
// unless there a reason it shouldn't be passed to dependencies.
196196
// (Note that at the time of writing, passing the other compiler args to dependencies is a no-op, since `--reachability=None` skips codegen anyway.)
197-
self.pkg_args.push(self.reachability_arg());
197+
let pkg_args = vec!["--".into(), self.reachability_arg()];
198198

199199
let mut found_target = false;
200200
let packages = self.packages_to_verify(&self.args, &metadata)?;
@@ -206,7 +206,7 @@ crate-type = ["lib"]
206206
cmd.args(&cargo_args)
207207
.args(vec!["-p", &package.id.to_string()])
208208
.args(verification_target.to_args())
209-
.args(&self.pkg_args)
209+
.args(&pkg_args)
210210
.env("RUSTC", &self.kani_compiler)
211211
// Use CARGO_ENCODED_RUSTFLAGS instead of RUSTFLAGS is preferred. See
212212
// https://doc.rust-lang.org/cargo/reference/environment-variables.html

kani-driver/src/call_single_file.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ impl KaniSession {
151151
flags.push("--no-assert-contracts".into());
152152
}
153153

154+
if let Some(args) = self.autoharness_compiler_flags.clone() {
155+
flags.extend(args);
156+
}
157+
154158
flags.extend(self.args.common_args.unstable_features.as_arguments().map(str::to_string));
155159

156160
flags

kani-driver/src/harness_runner.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,11 +298,11 @@ impl KaniSession {
298298
self.show_coverage_summary()?;
299299
}
300300

301-
if self.auto_harness {
301+
if self.autoharness_compiler_flags.is_some() {
302302
self.print_autoharness_summary(automatic)?;
303303
}
304304

305-
if failing > 0 && !self.auto_harness {
305+
if failing > 0 && self.autoharness_compiler_flags.is_none() {
306306
// Failure exit code without additional error message
307307
drop(self);
308308
std::process::exit(1);

0 commit comments

Comments
 (0)