Skip to content

Commit 210b6c3

Browse files
Skip codegen for unneeded harnesses (#4213)
This PR moves the logic for filtering out specific harnesses (as specified by the `--harness` & `--exact` Kani arguments) to the compiler, allowing us to skip codegen for harnesses that will end up being blocked by the filter. I’ve also added [a dev-build-only assertion](https://github.com/AlexanderPortland/kani/blob/c6283ac0ec53f70bf6ab78f367b6443c25b5290c/kani-driver/src/metadata.rs#L103-L110) to double check that we’re not doing codegen for any unused harnesses. Based on testing, something like running `cargo kani --harness estimator` on `s2n-quic-codec` used to take ~11s locally to compile 66 harnesses, but only 5 would end up being analyzed. Now, the same command compiles only the 5 needed harnesses, taking just ~5s. The downside of this is that the target crate now has to be recompiled if the user runs Kani with differing harness filters but, if the idea in #4212 is possible, that too could be avoided. 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 c0b5fda commit 210b6c3

File tree

8 files changed

+135
-72
lines changed

8 files changed

+135
-72
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,7 @@ dependencies = [
11991199
"serde",
12001200
"strum",
12011201
"strum_macros",
1202+
"tracing",
12021203
]
12031204

12041205
[[package]]

kani-compiler/src/args.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ pub struct Arguments {
5252
/// Option name used to use json pretty-print for output files.
5353
#[clap(long = "pretty-json-files")]
5454
pub output_pretty_json: bool,
55+
/// When specified, the harness filter will only match the exact fully qualified name of a harness.
56+
// (Passed here directly from [CargoKaniArgs] in `args_toml.rs`)
57+
#[arg(long, requires("harnesses"))]
58+
pub exact: bool,
59+
/// If specified, only run harnesses that match this filter. This option can be provided
60+
/// multiple times, which will run all tests matching any of the filters.
61+
/// If used with --exact, the harness filter will only match the exact fully qualified name of a harness.
62+
// (Passed here directly from [CargoKaniArgs] in `args_toml.rs`)
63+
#[arg(long = "harness", num_args(1), value_name = "HARNESS_FILTER")]
64+
pub harnesses: Vec<String>,
5565
/// Option used for suppressing global ASM error.
5666
#[clap(long)]
5767
pub ignore_global_asm: bool,

kani-compiler/src/kani_middle/codegen_units.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::kani_queries::QueryDb;
2121
use fxhash::{FxHashMap, FxHashSet};
2222
use kani_metadata::{
2323
ArtifactType, AssignsContract, AutoHarnessMetadata, AutoHarnessSkipReason, HarnessKind,
24-
HarnessMetadata, KaniMetadata,
24+
HarnessMetadata, KaniMetadata, find_proof_harnesses,
2525
};
2626
use regex::RegexSet;
2727
use rustc_hir::def_id::DefId;
@@ -75,15 +75,23 @@ impl CodegenUnits {
7575
let args = queries.args();
7676
match args.reachability_analysis {
7777
ReachabilityType::Harnesses => {
78-
let all_harnesses = get_all_manual_harnesses(tcx, base_filename);
78+
let all_harnesses = determine_targets(
79+
get_all_manual_harnesses(tcx, base_filename),
80+
&args.harnesses,
81+
args.exact,
82+
);
7983
// Even if no_stubs is empty we still need to store rustc metadata.
8084
let units = group_by_stubs(tcx, &all_harnesses);
8185
validate_units(tcx, &units);
8286
debug!(?units, "CodegenUnits::new");
8387
CodegenUnits { units, harness_info: all_harnesses, crate_info }
8488
}
8589
ReachabilityType::AllFns => {
86-
let mut all_harnesses = get_all_manual_harnesses(tcx, base_filename);
90+
let mut all_harnesses = determine_targets(
91+
get_all_manual_harnesses(tcx, base_filename),
92+
&args.harnesses,
93+
args.exact,
94+
);
8795
let mut units = group_by_stubs(tcx, &all_harnesses);
8896
validate_units(tcx, &units);
8997

@@ -328,6 +336,30 @@ fn get_all_manual_harnesses(
328336
.collect::<HashMap<_, _>>()
329337
}
330338

339+
/// Filter which harnesses to codegen based on user filters. Shares use of `find_proof_harnesses` with the `determine_targets` function
340+
/// in `kani-driver/src/metadata.rs` to ensure the filter is consistent and thus codegen is always done for the subset of harnesses we want
341+
/// to analyze.
342+
fn determine_targets(
343+
all_harnesses: HashMap<Harness, HarnessMetadata>,
344+
harness_filters: &[String],
345+
exact_filter: bool,
346+
) -> HashMap<Harness, HarnessMetadata> {
347+
if harness_filters.is_empty() {
348+
return all_harnesses;
349+
}
350+
351+
// If there are filters, only keep around harnesses that satisfy them.
352+
let mut new_harnesses = all_harnesses.clone();
353+
let valid_harnesses = find_proof_harnesses(
354+
&BTreeSet::from_iter(harness_filters.iter()),
355+
all_harnesses.values(),
356+
exact_filter,
357+
);
358+
359+
new_harnesses.retain(|_, metadata| valid_harnesses.contains(&&*metadata));
360+
new_harnesses
361+
}
362+
331363
/// For each function eligible for automatic verification,
332364
/// generate a harness Instance for it, then generate its metadata.
333365
/// Note that the body of each harness instance is still the dummy body of `kani_harness_intrinsic`;

kani-driver/src/call_single_file.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,14 @@ impl KaniSession {
162162
flags.push("--no-assert-contracts".into());
163163
}
164164

165+
for harness in &self.args.harnesses {
166+
flags.push(format!("--harness {harness}"));
167+
}
168+
169+
if self.args.exact {
170+
flags.push("--exact".into());
171+
}
172+
165173
if let Some(args) = self.autoharness_compiler_flags.clone() {
166174
flags.extend(args);
167175
}

kani-driver/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ fn standalone_main() -> Result<()> {
128128
/// Run verification on the given project.
129129
fn verify_project(project: Project, session: KaniSession) -> Result<()> {
130130
debug!(?project, "verify_project");
131-
let harnesses = session.determine_targets(&project.get_all_harnesses())?;
131+
let harnesses = session.determine_targets(project.get_all_harnesses())?;
132132
debug!(n = harnesses.len(), ?harnesses, "verify_project");
133133

134134
// Verification

kani-driver/src/metadata.rs

Lines changed: 33 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33

44
use anyhow::{Result, bail};
55
use std::path::Path;
6-
use tracing::{debug, trace};
76

8-
use kani_metadata::{HarnessMetadata, InternedString, TraitDefinedMethod, VtableCtxResults};
7+
use kani_metadata::{
8+
HarnessMetadata, InternedString, TraitDefinedMethod, VtableCtxResults, find_proof_harnesses,
9+
};
910
use std::collections::{BTreeSet, HashMap};
1011
use std::fs::File;
1112
use std::io::{BufReader, BufWriter};
@@ -91,39 +92,42 @@ impl KaniSession {
9192
/// Determine which function to use as entry point, based on command-line arguments and kani-metadata.
9293
pub fn determine_targets<'a>(
9394
&self,
94-
all_harnesses: &[&'a HarnessMetadata],
95+
compiler_filtered_harnesses: Vec<&'a HarnessMetadata>,
9596
) -> Result<Vec<&'a HarnessMetadata>> {
96-
let harnesses = BTreeSet::from_iter(self.args.harnesses.iter());
97-
let total_harnesses = harnesses.len();
98-
let all_targets = &harnesses;
97+
let harness_filters = BTreeSet::from_iter(self.args.harnesses.iter());
9998

100-
if harnesses.is_empty() {
101-
Ok(Vec::from(all_harnesses))
102-
} else {
103-
let harnesses_found: Vec<&HarnessMetadata> =
104-
find_proof_harnesses(&harnesses, all_harnesses, self.args.exact);
105-
106-
// If even one harness was not found with --exact, return an error to user
107-
if self.args.exact && harnesses_found.len() < total_harnesses {
108-
let harness_found_names: BTreeSet<&String> =
109-
harnesses_found.iter().map(|&h| &h.pretty_name).collect();
99+
// For dev builds, re-filter the harnesses to double check filtering in the compiler
100+
// and ensure we're doing the minimal harness codegen possible. That filtering happens in
101+
// the `kani-compiler/src/kani_middle/codegen_units.rs` file's `determine_targets` function.
102+
if cfg!(debug_assertions) && !harness_filters.is_empty() {
103+
let filtered_harnesses: Vec<&HarnessMetadata> = find_proof_harnesses(
104+
&harness_filters,
105+
compiler_filtered_harnesses.clone(),
106+
self.args.exact,
107+
);
108+
assert_eq!(compiler_filtered_harnesses, filtered_harnesses);
109+
}
110110

111-
// Check which harnesses are missing from the difference of targets and harnesses_found
112-
let harnesses_missing: Vec<&String> =
113-
all_targets.difference(&harness_found_names).cloned().collect();
114-
let joined_string = harnesses_missing
115-
.iter()
116-
.map(|&s| (*s).clone())
117-
.collect::<Vec<String>>()
118-
.join("`, `");
111+
// If any of the `--harness` filters failed to find a harness (and thus the # of harnesses is less than the # of filters), report that to the user.
112+
if self.args.exact && (compiler_filtered_harnesses.len() < self.args.harnesses.len()) {
113+
let harness_found_names: BTreeSet<&String> =
114+
compiler_filtered_harnesses.iter().map(|&h| &h.pretty_name).collect();
119115

120-
bail!(
121-
"Failed to match the following harness(es):\n{joined_string}\nPlease specify the fully-qualified name of a harness.",
122-
);
123-
}
116+
// Check which harnesses are missing from the difference of targets and all_harnesses
117+
let harnesses_missing: Vec<&String> =
118+
harness_filters.difference(&harness_found_names).cloned().collect();
119+
let joined_string = harnesses_missing
120+
.iter()
121+
.map(|&s| (*s).clone())
122+
.collect::<Vec<String>>()
123+
.join("`, `");
124124

125-
Ok(harnesses_found)
125+
bail!(
126+
"Failed to match the following harness(es):\n{joined_string}\nPlease specify the fully-qualified name of a harness.",
127+
);
126128
}
129+
130+
Ok(compiler_filtered_harnesses)
127131
}
128132
}
129133

@@ -142,44 +146,6 @@ pub fn sort_harnesses_by_loc<'a>(harnesses: &[&'a HarnessMetadata]) -> Vec<&'a H
142146
harnesses_clone
143147
}
144148

145-
/// Search for a proof harness with a particular name.
146-
/// At the present time, we use `no_mangle` so collisions shouldn't happen,
147-
/// but this function is written to be robust against that changing in the future.
148-
fn find_proof_harnesses<'a>(
149-
targets: &BTreeSet<&String>,
150-
all_harnesses: &[&'a HarnessMetadata],
151-
exact_filter: bool,
152-
) -> Vec<&'a HarnessMetadata> {
153-
debug!(?targets, "find_proof_harness");
154-
let mut result = vec![];
155-
for md in all_harnesses.iter() {
156-
// --harnesses should not select automatic harnesses
157-
if md.is_automatically_generated {
158-
continue;
159-
}
160-
if exact_filter {
161-
// Check for exact match only
162-
if targets.contains(&md.pretty_name) {
163-
// if exact match found, stop searching
164-
result.push(*md);
165-
} else {
166-
trace!(skip = md.pretty_name, "find_proof_harnesses");
167-
}
168-
} else {
169-
// Either an exact match, or a substring match. We check the exact first since it's cheaper.
170-
if targets.contains(&md.pretty_name)
171-
|| targets.contains(&md.get_harness_name_unqualified().to_string())
172-
|| targets.iter().any(|target| md.pretty_name.contains(*target))
173-
{
174-
result.push(*md);
175-
} else {
176-
trace!(skip = md.pretty_name, "find_proof_harnesses");
177-
}
178-
}
179-
}
180-
result
181-
}
182-
183149
#[cfg(test)]
184150
pub mod tests {
185151
use super::*;

kani_metadata/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ cbmc = { path = "../cprover_bindings", package = "cprover_bindings" }
1616
strum = "0.27.1"
1717
strum_macros = "0.27.1"
1818
clap = { version = "4.4.11", features = ["derive"] }
19+
tracing = "0.1.41"
1920

2021
[lints]
2122
workspace = true

kani_metadata/src/harness.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33

44
use crate::CbmcSolver;
55
use serde::{Deserialize, Serialize};
6-
use std::path::PathBuf;
6+
use std::{borrow::Borrow, collections::BTreeSet, path::PathBuf};
77
use strum_macros::Display;
8+
use tracing::{debug, trace};
89

910
/// A CBMC-level `assigns` contract that needs to be enforced on a function.
1011
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -112,3 +113,47 @@ impl HarnessMetadata {
112113
}
113114
}
114115
}
116+
117+
/// Search for a proof harness with a particular name.
118+
/// At the present time, we use `no_mangle` so collisions shouldn't happen,
119+
/// but this function is written to be robust against that changing in the future.
120+
pub fn find_proof_harnesses<'a, I>(
121+
targets: &BTreeSet<&String>,
122+
all_harnesses: I,
123+
exact_filter: bool,
124+
) -> Vec<&'a HarnessMetadata>
125+
where
126+
I: IntoIterator,
127+
I::Item: Borrow<&'a HarnessMetadata>,
128+
{
129+
debug!(?targets, "find_proof_harness");
130+
let mut result = vec![];
131+
for md in all_harnesses.into_iter() {
132+
let md: &'a HarnessMetadata = md.borrow();
133+
134+
// --harnesses should not select automatic harnesses
135+
if md.is_automatically_generated {
136+
continue;
137+
}
138+
if exact_filter {
139+
// Check for exact match only
140+
if targets.contains(&md.pretty_name) {
141+
// if exact match found, stop searching
142+
result.push(md);
143+
} else {
144+
trace!(skip = md.pretty_name, "find_proof_harnesses");
145+
}
146+
} else {
147+
// Either an exact match, or a substring match. We check the exact first since it's cheaper.
148+
if targets.contains(&md.pretty_name)
149+
|| targets.contains(&md.get_harness_name_unqualified().to_string())
150+
|| targets.iter().any(|target| md.pretty_name.contains(*target))
151+
{
152+
result.push(md);
153+
} else {
154+
trace!(skip = md.pretty_name, "find_proof_harnesses");
155+
}
156+
}
157+
}
158+
result
159+
}

0 commit comments

Comments
 (0)