Skip to content

Commit 033c954

Browse files
Compile harnesses with the same set of stubs together (rust-lang#2514)
Improve Kani compiler to compile harnesses that have exact the same set of stubs in a single Rust compiler session. This reduces the number of times we compile a crate when it has multiple harnesses use the same set of stubs. Co-authored-by: Zyad Hassan <88045115+zhassan-aws@users.noreply.github.com>
1 parent 5b116e2 commit 033c954

File tree

8 files changed

+214
-28
lines changed

8 files changed

+214
-28
lines changed

kani-compiler/src/kani_compiler.rs

Lines changed: 109 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ use rustc_interface::Config;
3333
use rustc_middle::ty::TyCtxt;
3434
use rustc_session::config::{ErrorOutputType, OutputType};
3535
use rustc_span::ErrorGuaranteed;
36-
use std::collections::HashMap;
36+
use std::collections::{BTreeMap, HashMap};
3737
use std::fs::File;
3838
use std::io::BufWriter;
3939
use std::mem;
@@ -67,7 +67,7 @@ fn backend(queries: Arc<Mutex<QueryDb>>) -> Box<CodegenBackend> {
6767
type HarnessId = DefPathHash;
6868

6969
/// A set of stubs.
70-
type Stubs = HashMap<DefPathHash, DefPathHash>;
70+
type Stubs = BTreeMap<DefPathHash, DefPathHash>;
7171

7272
#[derive(Debug)]
7373
struct HarnessInfo {
@@ -109,14 +109,15 @@ enum CompilationStage {
109109
/// Stage where the compiler will perform codegen of all harnesses that don't use stub.
110110
CodegenNoStubs {
111111
target_harnesses: Vec<HarnessId>,
112-
next_harnesses: Vec<HarnessId>,
112+
next_harnesses: Vec<Vec<HarnessId>>,
113113
all_harnesses: HashMap<HarnessId, HarnessInfo>,
114114
},
115-
/// Stage where the compiler will codegen harnesses that use stub, one at a time.
116-
/// Note: We could potentially codegen all harnesses that have the same list of stubs.
115+
/// Stage where the compiler will codegen harnesses that use stub, one group at a time.
116+
/// The harnesses at this stage are grouped according to the stubs they are using. For now,
117+
/// we ensure they have the exact same set of stubs.
117118
CodegenWithStubs {
118-
target_harness: HarnessId,
119-
next_harnesses: Vec<HarnessId>,
119+
target_harnesses: Vec<HarnessId>,
120+
next_harnesses: Vec<Vec<HarnessId>>,
120121
all_harnesses: HashMap<HarnessId, HarnessInfo>,
121122
},
122123
Done,
@@ -166,7 +167,9 @@ impl KaniCompiler {
166167
CompilationStage::CodegenNoStubs { .. } => {
167168
unreachable!("This stage should always run in the same session as Init");
168169
}
169-
CompilationStage::CodegenWithStubs { target_harness, all_harnesses, .. } => {
170+
CompilationStage::CodegenWithStubs { target_harnesses, all_harnesses, .. } => {
171+
assert!(!target_harnesses.is_empty(), "expected at least one target harness");
172+
let target_harness = &target_harnesses[0];
170173
let extra_arg =
171174
stubbing::mk_rustc_arg(&all_harnesses[&target_harness].stub_map);
172175
let mut args = orig_args.clone();
@@ -192,9 +195,10 @@ impl KaniCompiler {
192195
}
193196
CompilationStage::CodegenNoStubs { next_harnesses, all_harnesses, .. }
194197
| CompilationStage::CodegenWithStubs { next_harnesses, all_harnesses, .. } => {
195-
if let Some(target_harness) = next_harnesses.pop() {
198+
if let Some(target_harnesses) = next_harnesses.pop() {
199+
assert!(!target_harnesses.is_empty(), "expected at least one target harness");
196200
CompilationStage::CodegenWithStubs {
197-
target_harness,
201+
target_harnesses,
198202
next_harnesses: mem::take(next_harnesses),
199203
all_harnesses: mem::take(all_harnesses),
200204
}
@@ -210,6 +214,7 @@ impl KaniCompiler {
210214

211215
/// Run the Rust compiler with the given arguments and pass `&mut self` to handle callbacks.
212216
fn run_compilation_session(&mut self, args: &[String]) -> Result<(), ErrorGuaranteed> {
217+
debug!(?args, "run_compilation_session");
213218
let queries = self.queries.clone();
214219
let mut compiler = RunCompiler::new(args, self);
215220
compiler.set_make_codegen_backend(Some(Box::new(move |_cfg| backend(queries))));
@@ -249,7 +254,7 @@ impl KaniCompiler {
249254
// Even if no_stubs is empty we still need to store metadata.
250255
CompilationStage::CodegenNoStubs {
251256
target_harnesses: no_stubs,
252-
next_harnesses: with_stubs,
257+
next_harnesses: group_by_stubs(with_stubs, &all_harnesses),
253258
all_harnesses,
254259
}
255260
} else {
@@ -266,7 +271,15 @@ impl KaniCompiler {
266271
fn prepare_codegen(&mut self) -> Compilation {
267272
debug!(stage=?self.stage, "prepare_codegen");
268273
match &self.stage {
269-
CompilationStage::CodegenNoStubs { target_harnesses, all_harnesses, .. } => {
274+
CompilationStage::CodegenNoStubs { target_harnesses, all_harnesses, .. }
275+
| CompilationStage::CodegenWithStubs { target_harnesses, all_harnesses, .. } => {
276+
debug!(
277+
harnesses=?target_harnesses
278+
.iter()
279+
.map(|h| &all_harnesses[h].metadata.pretty_name)
280+
.collect::<Vec<_>>(),
281+
"prepare_codegen"
282+
);
270283
let queries = &mut (*self.queries.lock().unwrap());
271284
queries.harnesses_info = target_harnesses
272285
.iter()
@@ -276,14 +289,6 @@ impl KaniCompiler {
276289
.collect();
277290
Compilation::Continue
278291
}
279-
CompilationStage::CodegenWithStubs { target_harness, all_harnesses, .. } => {
280-
let queries = &mut (*self.queries.lock().unwrap());
281-
queries.harnesses_info = HashMap::from([(
282-
*target_harness,
283-
all_harnesses[&target_harness].metadata.goto_file.clone().unwrap(),
284-
)]);
285-
Compilation::Continue
286-
}
287292
CompilationStage::Init | CompilationStage::Done => unreachable!(),
288293
}
289294
}
@@ -314,6 +319,18 @@ impl KaniCompiler {
314319
}
315320
}
316321

322+
/// Group the harnesses by their stubs.
323+
fn group_by_stubs(
324+
harnesses: Vec<HarnessId>,
325+
all_harnesses: &HashMap<HarnessId, HarnessInfo>,
326+
) -> Vec<Vec<HarnessId>> {
327+
let mut per_stubs: BTreeMap<&Stubs, Vec<HarnessId>> = BTreeMap::default();
328+
for harness in harnesses {
329+
per_stubs.entry(&all_harnesses[&harness].stub_map).or_default().push(harness)
330+
}
331+
per_stubs.into_values().collect()
332+
}
333+
317334
/// Use default function implementations.
318335
impl Callbacks for KaniCompiler {
319336
/// Configure the [KaniCompiler] `self` object during the [CompilationStage::Init].
@@ -326,7 +343,6 @@ impl Callbacks for KaniCompiler {
326343
&matches,
327344
matches!(config.opts.error_format, ErrorOutputType::Json { .. }),
328345
);
329-
330346
// Configure queries.
331347
let queries = &mut (*self.queries.lock().unwrap());
332348
queries.emit_vtable_restrictions = matches.get_flag(parser::RESTRICT_FN_PTRS);
@@ -364,3 +380,75 @@ impl Callbacks for KaniCompiler {
364380
self.prepare_codegen()
365381
}
366382
}
383+
384+
#[cfg(test)]
385+
mod tests {
386+
use super::{HarnessInfo, Stubs};
387+
use crate::kani_compiler::{group_by_stubs, HarnessId};
388+
use kani_metadata::{HarnessAttributes, HarnessMetadata};
389+
use rustc_data_structures::fingerprint::Fingerprint;
390+
use rustc_hir::definitions::DefPathHash;
391+
use std::collections::HashMap;
392+
393+
fn mock_next_id() -> HarnessId {
394+
static mut COUNTER: u64 = 0;
395+
unsafe { COUNTER += 1 };
396+
let id = unsafe { COUNTER };
397+
DefPathHash(Fingerprint::new(id, 0))
398+
}
399+
400+
fn mock_metadata() -> HarnessMetadata {
401+
HarnessMetadata {
402+
pretty_name: String::from("dummy"),
403+
mangled_name: String::from("dummy"),
404+
crate_name: String::from("dummy"),
405+
original_file: String::from("dummy"),
406+
original_start_line: 10,
407+
original_end_line: 20,
408+
goto_file: None,
409+
attributes: HarnessAttributes::default(),
410+
}
411+
}
412+
413+
fn mock_info_with_stubs(stub_map: Stubs) -> HarnessInfo {
414+
HarnessInfo { metadata: mock_metadata(), stub_map }
415+
}
416+
417+
#[test]
418+
fn test_group_by_stubs_works() {
419+
// Set up the inputs
420+
let harness_1 = mock_next_id();
421+
let harness_2 = mock_next_id();
422+
let harness_3 = mock_next_id();
423+
let harnesses = vec![harness_1, harness_2, harness_3];
424+
425+
let stub_1 = (mock_next_id(), mock_next_id());
426+
let stub_2 = (mock_next_id(), mock_next_id());
427+
let stub_3 = (mock_next_id(), mock_next_id());
428+
let stub_4 = (stub_3.0, mock_next_id());
429+
430+
let set_1 = Stubs::from([stub_1, stub_2, stub_3]);
431+
let set_2 = Stubs::from([stub_1, stub_2, stub_4]);
432+
let set_3 = Stubs::from([stub_1, stub_3, stub_2]);
433+
assert_eq!(set_1, set_3);
434+
assert_ne!(set_1, set_2);
435+
436+
let harnesses_info = HashMap::from([
437+
(harness_1, mock_info_with_stubs(set_1)),
438+
(harness_2, mock_info_with_stubs(set_2)),
439+
(harness_3, mock_info_with_stubs(set_3)),
440+
]);
441+
assert_eq!(harnesses_info.len(), 3);
442+
443+
// Run the function under test.
444+
let grouped = group_by_stubs(harnesses, &harnesses_info);
445+
446+
// Verify output.
447+
assert_eq!(grouped.len(), 2);
448+
assert!(
449+
grouped.contains(&vec![harness_1, harness_3])
450+
|| grouped.contains(&vec![harness_3, harness_1])
451+
);
452+
assert!(grouped.contains(&vec![harness_2]));
453+
}
454+
}

kani-compiler/src/kani_middle/stubbing/annotations.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
33
//! This file contains code for extracting stubbing-related attributes.
44
5-
use std::collections::HashMap;
5+
use std::collections::BTreeMap;
66

77
use kani_metadata::Stub;
88
use rustc_hir::def_id::{DefId, LocalDefId};
@@ -42,7 +42,7 @@ pub fn update_stub_mapping(
4242
tcx: TyCtxt,
4343
harness: LocalDefId,
4444
stub: &Stub,
45-
stub_pairs: &mut HashMap<DefPathHash, DefPathHash>,
45+
stub_pairs: &mut BTreeMap<DefPathHash, DefPathHash>,
4646
) {
4747
if let Some((orig_id, stub_id)) = stub_def_ids(tcx, harness, stub) {
4848
let orig_hash = tcx.def_path_hash(orig_id);

kani-compiler/src/kani_middle/stubbing/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
mod annotations;
66
mod transform;
77

8-
use std::collections::HashMap;
8+
use std::collections::BTreeMap;
99

1010
use kani_metadata::HarnessMetadata;
1111
use rustc_hir::def_id::DefId;
@@ -20,9 +20,9 @@ pub fn harness_stub_map(
2020
tcx: TyCtxt,
2121
harness: DefId,
2222
metadata: &HarnessMetadata,
23-
) -> HashMap<DefPathHash, DefPathHash> {
23+
) -> BTreeMap<DefPathHash, DefPathHash> {
2424
let attrs = &metadata.attributes;
25-
let mut stub_pairs = HashMap::default();
25+
let mut stub_pairs = BTreeMap::default();
2626
for stubs in &attrs.stubs {
2727
update_stub_mapping(tcx, harness.expect_local(), stubs, &mut stub_pairs);
2828
}

kani-compiler/src/kani_middle/stubbing/transform.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
//! body of its stub, if appropriate. The stub mapping it uses is set via rustc
77
//! arguments.
88
9-
use std::collections::HashMap;
9+
use std::collections::{BTreeMap, HashMap};
1010

1111
use lazy_static::lazy_static;
1212
use regex::Regex;
@@ -115,7 +115,7 @@ fn check_compatibility<'a, 'tcx>(
115115
const RUSTC_ARG_PREFIX: &str = "kani_stubs=";
116116

117117
/// Serializes the stub mapping into a rustc argument.
118-
pub fn mk_rustc_arg(stub_mapping: &HashMap<DefPathHash, DefPathHash>) -> String {
118+
pub fn mk_rustc_arg(stub_mapping: &BTreeMap<DefPathHash, DefPathHash>) -> String {
119119
// Serialize each `DefPathHash` as a pair of `u64`s, and the whole mapping
120120
// as an association list.
121121
let mut pairs = Vec::new();
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Complete - 6 successfully verified harnesses, 0 failures, 6 total.
2+
3+
Rust compiler sessions: 4
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env bash
2+
# Copyright Kani Contributors
3+
# SPDX-License-Identifier: Apache-2.0 OR MIT
4+
#
5+
# Checks that the Kani compiler can encode harnesses with the same set of stubs
6+
# in one rustc session.
7+
8+
set +e
9+
10+
log_file=output.log
11+
12+
kani stubbing.rs --enable-unstable --enable-stubbing --verbose >& ${log_file}
13+
14+
echo "------- Raw output ---------"
15+
cat $log_file
16+
echo "----------------------------"
17+
18+
# We print the reachability analysis results once for each session.
19+
# This is the only reliable way to get the number of sessions from the compiler.
20+
# The other option would be to use debug comments.
21+
# Ideally, the compiler should only print one set of statistics at the end of its run.
22+
# In that case, we should include number of sessions to those stats.
23+
runs=$(grep -c "Reachability Analysis Result" ${log_file})
24+
echo "Rust compiler sessions: ${runs}"
25+
26+
# Cleanup
27+
rm ${log_file}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Copyright Kani Contributors
2+
# SPDX-License-Identifier: Apache-2.0 OR MIT
3+
script: check_compiler_sessions.sh
4+
expected: check_compiler_sessions.expected
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
//
4+
// Check that Kani handles different sets of stubbing correctly.
5+
// I.e., not correctly replacing the stubs will cause a harness to fail.
6+
7+
fn identity(i: i8) -> i8 {
8+
i
9+
}
10+
11+
fn decrement(i: i8) -> i8 {
12+
i.wrapping_sub(1)
13+
}
14+
15+
fn increment(i: i8) -> i8 {
16+
i.wrapping_add(1)
17+
}
18+
19+
#[kani::proof]
20+
fn check_identity() {
21+
let n = kani::any();
22+
assert_eq!(identity(n), n);
23+
}
24+
25+
#[kani::proof]
26+
fn check_decrement() {
27+
let n = kani::any();
28+
kani::assume(n > i8::MIN);
29+
assert_eq!(decrement(n), n - 1);
30+
}
31+
32+
#[kani::proof]
33+
#[kani::stub(decrement, increment)]
34+
fn check_decrement_is_increment() {
35+
let n = kani::any();
36+
kani::assume(n < i8::MAX);
37+
assert_eq!(decrement(n), n + 1);
38+
}
39+
40+
#[kani::proof]
41+
#[kani::stub(increment, identity)]
42+
#[kani::stub(decrement, identity)]
43+
fn check_all_identity() {
44+
let n = kani::any();
45+
assert_eq!(decrement(n), increment(n));
46+
}
47+
48+
#[kani::proof]
49+
#[kani::stub(decrement, identity)]
50+
#[kani::stub(increment, identity)]
51+
fn check_all_identity_2() {
52+
let n = kani::any();
53+
assert_eq!(decrement(n), n);
54+
assert_eq!(increment(n), n);
55+
}
56+
57+
#[kani::proof]
58+
#[kani::stub(decrement, increment)]
59+
#[kani::stub(increment, identity)]
60+
fn check_indirect_all_identity() {
61+
let n = kani::any();
62+
assert_eq!(decrement(n), n);
63+
assert_eq!(increment(n), n);
64+
}

0 commit comments

Comments
 (0)