Skip to content

Commit 0024103

Browse files
authored
Autoharness: Derive Arbitrary for structs and enums (#4167)
While generating automatic harnesses, derive `Arbitrary` implementations for structs and enums that don't have implementations. ## Implementation Overview 1. In `automatic_harness_partition`, we mark a function as eligible for autoharness if its arguments either: a. implement `Arbitrary`, or b. are structs/enums whose fields implement `Arbitrary`. 2. `AutomaticHarnessPass` runs as before, but now may generate harnesses that call `kani::any()` for structs/enums that do not actually implement `Arbitrary`. See the definition of `kani::any()`: https://github.com/model-checking/kani/blob/b64e59de669cd77b625cc8c0b9a94f29117a0ff7/library/kani_core/src/lib.rs#L274-L276 The initial `kani::any()` call resolves fine, but the compiler would ICE if it tried to resolve `T::any()`. 3. To avoid the ICE, we add a new `AutomaticArbitraryPass` that runs after the `AutomaticHarnessPass`. This pass detects the calls to nonexistent `T::any()`s and replaces them with inlined `T::any()` implementations. These implementations are based on what Kani's derive macros for structs and enums produce. ## Example Given the automatic harness `foo_harness` produced by `AutomaticHarnessPass`: ```rust struct Foo(u32) fn function_with_foo(foo: Foo) { ... } // pretend this is an autoharness, just written in source code for the example #[kani::proof] fn foo_harness() { let foo = kani::any(); function_with_foo(foo); } ``` `AutomaticArbitraryPass` will rewrite `kani::any()`: ```rust pub fn any() -> Foo { Foo::any() } ``` as: ```rust pub fn any() -> Foo { Self(kani::any()) } ``` i.e., replace the call to `Foo::any()` with what the derive macro would have produced for the body of `Foo::any()` (had the programmer used the derive macro instead). ## Limitations The fields need to have `Arbitrary` implementations; there is no support for recursive derivation. See the tests for examples; this should be resolvable through further engineering effort. Towards #3832 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 ada8ed3 commit 0024103

File tree

20 files changed

+819
-61
lines changed

20 files changed

+819
-61
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,6 +1084,7 @@ dependencies = [
10841084
"charon",
10851085
"clap",
10861086
"cprover_bindings",
1087+
"fxhash",
10871088
"itertools 0.14.0",
10881089
"kani_metadata",
10891090
"lazy_static",

docs/src/reference/experimental/autoharness.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,10 @@ please add them to [this GitHub issue](https://github.com/model-checking/kani/is
103103

104104
## Limitations
105105
### Arguments Implementing Arbitrary
106-
Kani will only generate an automatic harness for a function if it can determine that all of the function's arguments implement Arbitrary.
107-
It does not attempt to derive/implement Arbitrary for any types, even if those types could implement Arbitrary.
108-
For example, imagine a user defines `struct MyStruct { x: u8, y: u8}`, but does not derive or implement Arbitrary for `MyStruct`.
109-
Kani does not attempt to add such derivations itself, so it will not generate a harness for a function that takes `MyStruct` as input.
106+
Kani will only generate an automatic harness for a function if it can represent each of its arguments nondeterministically, without bounds.
107+
In technical terms, each of the arguments needs to implement the `Arbitrary` trait or be capable of deriving it.
108+
Kani will detect if a struct or enum could implement `Arbitrary` and derive it automatically.
109+
Note that this automatic derivation feature is only available for autoharness.
110110

111111
### Generic Functions
112112
The current implementation does not generate harnesses for generic functions.

kani-compiler/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ publish = false
1212
cbmc = { path = "../cprover_bindings", package = "cprover_bindings", optional = true }
1313
charon = { path = "../charon/charon", optional = true, default-features = false }
1414
clap = { version = "4.4.11", features = ["derive", "cargo"] }
15+
fxhash = "0.2.1"
1516
itertools = "0.14"
1617
kani_metadata = { path = "../kani_metadata" }
1718
lazy_static = "1.5.0"

kani-compiler/src/kani_middle/codegen_units.rs

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ use crate::kani_middle::metadata::{
1616
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};
19+
use crate::kani_middle::{can_derive_arbitrary, implements_arbitrary};
1920
use crate::kani_queries::QueryDb;
21+
use fxhash::FxHashMap;
2022
use kani_metadata::{
2123
ArtifactType, AssignsContract, AutoHarnessMetadata, AutoHarnessSkipReason, HarnessKind,
2224
HarnessMetadata, KaniMetadata,
@@ -26,8 +28,8 @@ use rustc_hir::def_id::DefId;
2628
use rustc_middle::ty::TyCtxt;
2729
use rustc_session::config::OutputType;
2830
use rustc_smir::rustc_internal;
29-
use stable_mir::mir::{TerminatorKind, mono::Instance};
30-
use stable_mir::ty::{FnDef, GenericArgKind, GenericArgs, IndexedVal, RigidTy, TyKind};
31+
use stable_mir::mir::mono::Instance;
32+
use stable_mir::ty::{FnDef, GenericArgKind, GenericArgs, IndexedVal, RigidTy, Ty, TyKind};
3133
use stable_mir::{CrateDef, CrateItem};
3234
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
3335
use std::fs::File;
@@ -394,10 +396,13 @@ fn automatic_harness_partition(
394396
let included_set = make_regex_set(args.autoharness_included_patterns.clone());
395397
let excluded_set = make_regex_set(args.autoharness_excluded_patterns.clone());
396398

399+
// Cache whether a type implements or can derive Arbitrary
400+
let mut ty_arbitrary_cache: FxHashMap<Ty, bool> = FxHashMap::default();
401+
397402
// If `func` is not eligible for an automatic harness, return the reason why; if it is eligible, return None.
398403
// Note that we only return one reason for ineligiblity, when there could be multiple;
399404
// we can revisit this implementation choice in the future if users request more verbose output.
400-
let skip_reason = |fn_item: CrateItem| -> Option<AutoHarnessSkipReason> {
405+
let mut skip_reason = |fn_item: CrateItem| -> Option<AutoHarnessSkipReason> {
401406
if KaniAttributes::for_def_id(tcx, fn_item.def_id()).is_kani_instrumentation() {
402407
return Some(AutoHarnessSkipReason::KaniImpl);
403408
}
@@ -432,25 +437,12 @@ fn automatic_harness_partition(
432437
// Note that we've already filtered out generic functions, so we know that each of these arguments has a concrete type.
433438
let mut problematic_args = vec![];
434439
for (idx, arg) in body.arg_locals().iter().enumerate() {
435-
let kani_any_body =
436-
Instance::resolve(kani_any_def, &GenericArgs(vec![GenericArgKind::Type(arg.ty)]))
437-
.unwrap()
438-
.body()
439-
.unwrap();
440-
441-
let implements_arbitrary = if let TerminatorKind::Call { func, .. } =
442-
&kani_any_body.blocks[0].terminator.kind
443-
{
444-
if let Some((def, args)) = func.ty(body.arg_locals()).unwrap().kind().fn_def() {
445-
Instance::resolve(def, args).is_ok()
446-
} else {
447-
false
448-
}
449-
} else {
450-
false
451-
};
452-
453-
if !implements_arbitrary {
440+
let implements_arbitrary = ty_arbitrary_cache.entry(arg.ty).or_insert_with(|| {
441+
implements_arbitrary(arg.ty, kani_any_def)
442+
|| can_derive_arbitrary(arg.ty, kani_any_def)
443+
});
444+
445+
if !(*implements_arbitrary) {
454446
// Find the name of the argument by referencing var_debug_info.
455447
// Note that enumerate() starts at 0, while StableMIR argument_index starts at 1, hence the idx+1.
456448
let arg_name = body

kani-compiler/src/kani_middle/mod.rs

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ use crate::kani_queries::QueryDb;
99
use rustc_hir::{def::DefKind, def_id::DefId as InternalDefId, def_id::LOCAL_CRATE};
1010
use rustc_middle::ty::TyCtxt;
1111
use rustc_smir::rustc_internal;
12-
use stable_mir::mir::mono::MonoItem;
13-
use stable_mir::ty::{FnDef, RigidTy, Span as SpanStable, Ty, TyKind};
12+
use stable_mir::mir::TerminatorKind;
13+
use stable_mir::mir::mono::{Instance, MonoItem};
14+
use stable_mir::ty::{
15+
AdtDef, AdtKind, FnDef, GenericArgKind, GenericArgs, RigidTy, Span as SpanStable, Ty, TyKind,
16+
};
1417
use stable_mir::visitor::{Visitable, Visitor as TyVisitor};
1518
use stable_mir::{CrateDef, DefId};
1619
use std::ops::ControlFlow;
@@ -165,3 +168,65 @@ pub fn stable_fn_def(tcx: TyCtxt, def_id: InternalDefId) -> Option<FnDef> {
165168
None
166169
}
167170
}
171+
172+
/// Inspect a `kani::any<T>()` call to determine if `T: Arbitrary`
173+
/// `kani_any_def` refers to a function that looks like:
174+
/// ```rust
175+
/// fn any<T: Arbitrary>() -> T {
176+
/// T::any()
177+
/// }
178+
/// ```
179+
/// So we select the terminator that calls T::kani::Arbitrary::any(), then try to resolve it to an Instance.
180+
/// `T` implements Arbitrary iff we successfully resolve the Instance.
181+
fn implements_arbitrary(ty: Ty, kani_any_def: FnDef) -> bool {
182+
let kani_any_body =
183+
Instance::resolve(kani_any_def, &GenericArgs(vec![GenericArgKind::Type(ty)]))
184+
.unwrap()
185+
.body()
186+
.unwrap();
187+
188+
for bb in kani_any_body.blocks.iter() {
189+
let TerminatorKind::Call { func, .. } = &bb.terminator.kind else {
190+
continue;
191+
};
192+
if let TyKind::RigidTy(RigidTy::FnDef(def, args)) =
193+
func.ty(kani_any_body.arg_locals()).unwrap().kind()
194+
{
195+
return Instance::resolve(def, &args).is_ok();
196+
}
197+
}
198+
false
199+
}
200+
201+
/// Is `ty` a struct or enum whose fields/variants implement Arbitrary?
202+
fn can_derive_arbitrary(ty: Ty, kani_any_def: FnDef) -> bool {
203+
let variants_can_derive = |def: AdtDef| {
204+
for variant in def.variants_iter() {
205+
let fields = variant.fields();
206+
let mut fields_impl_arbitrary = true;
207+
for ty in fields.iter().map(|field| field.ty()) {
208+
fields_impl_arbitrary &= implements_arbitrary(ty, kani_any_def);
209+
}
210+
if !fields_impl_arbitrary {
211+
return false;
212+
}
213+
}
214+
true
215+
};
216+
217+
if let TyKind::RigidTy(RigidTy::Adt(def, _)) = ty.kind() {
218+
match def.kind() {
219+
AdtKind::Enum => {
220+
// Enums with no variants cannot be instantiated
221+
if def.num_variants() == 0 {
222+
return false;
223+
}
224+
variants_can_derive(def)
225+
}
226+
AdtKind::Struct => variants_can_derive(def),
227+
AdtKind::Union => false,
228+
}
229+
} else {
230+
false
231+
}
232+
}

0 commit comments

Comments
 (0)