Skip to content

Commit 62b66de

Browse files
author
Carolyn Zech
committed
Verify contracts on methods where the base type has multiple inherent impls
Add a base_path_args field to the Path struct so that we can use the generic arguments to choose the appropriate implementation
1 parent 690ba97 commit 62b66de

File tree

8 files changed

+236
-16
lines changed

8 files changed

+236
-16
lines changed

kani-compiler/src/kani_middle/resolve.rs

Lines changed: 97 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use stable_mir::ty::{FnDef, RigidTy, Ty, TyKind};
2424
use std::collections::HashSet;
2525
use std::fmt;
2626
use std::iter::Peekable;
27-
use syn::{PathSegment, QSelf, TypePath};
27+
use syn::{PathArguments, PathSegment, QSelf, TypePath};
2828
use tracing::{debug, debug_span};
2929

3030
mod type_resolution;
@@ -153,7 +153,7 @@ fn resolve_path<'tcx>(
153153
let next_item = match def_kind {
154154
DefKind::ForeignMod | DefKind::Mod => resolve_in_module(tcx, base, &name),
155155
DefKind::Struct | DefKind::Enum | DefKind::Union => {
156-
resolve_in_type_def(tcx, base, &name)
156+
resolve_in_type_def(tcx, base, &path.base_path_args, &name)
157157
}
158158
DefKind::Trait => resolve_in_trait(tcx, base, &name),
159159
kind => {
@@ -246,12 +246,13 @@ impl fmt::Display for ResolveError<'_> {
246246
/// The segments of a path.
247247
type Segments = Vec<PathSegment>;
248248

249-
/// A path consisting of a starting point and a bunch of segments. If `base`
249+
/// A path consisting of a starting point, any PathArguments for that starting point, and a bunch of segments. If `base`
250250
/// matches `Base::LocalModule { id: _, may_be_external_path : true }`, then
251251
/// `segments` cannot be empty.
252252
#[derive(Debug, Hash)]
253253
struct Path {
254254
pub base: DefId,
255+
pub base_path_args: PathArguments,
255256
pub segments: Segments,
256257
}
257258

@@ -285,7 +286,11 @@ fn resolve_prefix<'tcx>(
285286
let next_name = segment.ident.to_string();
286287
let result = resolve_external(tcx, &next_name);
287288
if let Some(def_id) = result {
288-
Ok(Path { base: def_id, segments: segments.cloned().collect() })
289+
Ok(Path {
290+
base: def_id,
291+
base_path_args: segment.arguments.clone(),
292+
segments: segments.cloned().collect(),
293+
})
289294
} else {
290295
Err(ResolveError::MissingItem {
291296
tcx,
@@ -306,7 +311,11 @@ fn resolve_prefix<'tcx>(
306311
None => current_module,
307312
Some((hir_id, _)) => hir_id.owner.def_id,
308313
};
309-
Ok(Path { base: crate_root.to_def_id(), segments: segments.cloned().collect() })
314+
Ok(Path {
315+
base: crate_root.to_def_id(),
316+
base_path_args: segment.arguments.clone(),
317+
segments: segments.cloned().collect(),
318+
})
310319
}
311320
// Path starting with "self::"
312321
(None, Some(segment)) if segment.ident == SELF => {
@@ -334,7 +343,11 @@ fn resolve_prefix<'tcx>(
334343
Err(err)
335344
}
336345
})?;
337-
Ok(Path { base: def_id, segments: segments.cloned().collect() })
346+
Ok(Path {
347+
base: def_id,
348+
base_path_args: segment.arguments.clone(),
349+
segments: segments.cloned().collect(),
350+
})
338351
}
339352
_ => {
340353
unreachable!("Empty path: `{path:?}`")
@@ -364,7 +377,11 @@ where
364377
}
365378
}
366379
debug!("base: {base_module:?}");
367-
Ok(Path { base: base_module.to_def_id(), segments: segments.cloned().collect() })
380+
Ok(Path {
381+
base: base_module.to_def_id(),
382+
base_path_args: PathArguments::None,
383+
segments: segments.cloned().collect(),
384+
})
368385
}
369386

370387
/// Resolves an external crate name.
@@ -543,14 +560,19 @@ where
543560
if segments.next().is_some() {
544561
Err(ResolveError::UnexpectedType { tcx, item: def_id, expected: "module" })
545562
} else {
546-
resolve_in_type_def(tcx, def_id, &name.ident.to_string())
563+
resolve_in_type_def(tcx, def_id, &PathArguments::None, &name.ident.to_string())
547564
}
548565
}
549566

567+
fn generic_args_to_string<T: ToTokens>(args: &T) -> String {
568+
args.to_token_stream().to_string().chars().filter(|c| !c.is_whitespace()).collect::<String>()
569+
}
570+
550571
/// Resolves a function in a type given its `def_id`.
551572
fn resolve_in_type_def<'tcx>(
552573
tcx: TyCtxt<'tcx>,
553574
type_id: DefId,
575+
base_path_args: &PathArguments,
554576
name: &str,
555577
) -> Result<DefId, ResolveError<'tcx>> {
556578
debug!(?name, ?type_id, "resolve_in_type");
@@ -566,12 +588,57 @@ fn resolve_in_type_def<'tcx>(
566588
match candidates.len() {
567589
0 => Err(ResolveError::MissingItem { tcx, base: type_id, unresolved: name.to_string() }),
568590
1 => Ok(candidates[0]),
569-
_ => Err(ResolveError::AmbiguousPartialPath {
570-
tcx,
571-
name: name.into(),
572-
base: type_id,
573-
candidates,
574-
}),
591+
_ => {
592+
let invalid_path_err = |generic_args, candidates: Vec<DefId>| -> ResolveError {
593+
ResolveError::InvalidPath {
594+
msg: format!(
595+
"the generic arguments {} are invalid. The available implementations are: \n{}",
596+
&generic_args,
597+
&candidates
598+
.iter()
599+
.map(|def_id| tcx.def_path_str(def_id))
600+
.intersperse("\n".to_string())
601+
.collect::<String>()
602+
),
603+
}
604+
};
605+
// If there are multiple implementations, we need generic arguments on the base type to refine our options.
606+
match base_path_args {
607+
// If there aren't such arguments, report the ambiguity.
608+
PathArguments::None => Err(ResolveError::AmbiguousPartialPath {
609+
tcx,
610+
name: name.into(),
611+
base: type_id,
612+
candidates,
613+
}),
614+
// Otherwise, use the provided generic arguments to refine our options.
615+
PathArguments::AngleBracketed(args) => {
616+
let generic_args = generic_args_to_string(&args);
617+
let refined_candidates: Vec<DefId> = candidates
618+
.iter()
619+
.cloned()
620+
.filter(|item| {
621+
is_item_name_with_generic_args(tcx, *item, &generic_args, name)
622+
})
623+
.collect();
624+
match refined_candidates.len() {
625+
0 => Err(invalid_path_err(&generic_args, candidates)),
626+
1 => Ok(refined_candidates[0]),
627+
// since is_item_name_with_generic_args looks at the entire item path after the base type, it shouldn't be possible to have more than one match
628+
_ => unreachable!(
629+
"Got multiple refined candidates {:?}",
630+
refined_candidates
631+
.iter()
632+
.map(|def_id| tcx.def_path_str(def_id))
633+
.collect::<Vec<String>>()
634+
),
635+
}
636+
}
637+
PathArguments::Parenthesized(args) => {
638+
Err(invalid_path_err(&generic_args_to_string(args), candidates))
639+
}
640+
}
641+
}
575642
}
576643
}
577644

@@ -625,7 +692,11 @@ where
625692
base: ty,
626693
unresolved: name.to_string(),
627694
})?;
628-
Ok(Path { base: item, segments: segments.cloned().collect() })
695+
Ok(Path {
696+
base: item,
697+
base_path_args: PathArguments::None,
698+
segments: segments.cloned().collect(),
699+
})
629700
} else {
630701
Err(ResolveError::InvalidPath { msg: format!("Unexpected primitive type `{ty}`") })
631702
}
@@ -636,3 +707,14 @@ fn is_item_name(tcx: TyCtxt, item: DefId, name: &str) -> bool {
636707
let last = item_path.split("::").last().unwrap();
637708
last == name
638709
}
710+
711+
fn is_item_name_with_generic_args(
712+
tcx: TyCtxt,
713+
item: DefId,
714+
generic_args: &str,
715+
name: &str,
716+
) -> bool {
717+
let item_path = tcx.def_path_str(item);
718+
let all_but_base_type = item_path.find("::").map_or("", |idx| &item_path[idx..]);
719+
all_but_base_type == format!("{}::{}", generic_args, name)
720+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
// kani-flags: -Zfunction-contracts
4+
5+
// Check that Kani can verify contracts on methods where the base type has multiple inherent impls,
6+
// c.f. https://github.com/model-checking/kani/issues/3773
7+
8+
struct NonZero<T>(T);
9+
10+
impl NonZero<u32> {
11+
#[kani::requires(self.0.checked_mul(x).is_some())]
12+
fn unchecked_mul(self, x: u32) -> u32 {
13+
self.0 * x
14+
}
15+
}
16+
17+
impl NonZero<i32> {
18+
#[kani::requires(self.0.checked_mul(x).is_some())]
19+
fn unchecked_mul(self, x: i32) -> i32 {
20+
self.0 * x
21+
}
22+
}
23+
24+
#[kani::proof_for_contract(NonZero::<i32>::unchecked_mul)]
25+
fn verify_unchecked_mul_ambiguous_path() {
26+
let x: NonZero<i32> = NonZero(-1);
27+
x.unchecked_mul(-2);
28+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Failed to resolve checking function NonZero::<g32>::unchecked_mul because the generic arguments ::<g32> are invalid. The available implementations are:
2+
NonZero::<u32>::unchecked_mul
3+
NonZero::<i32>::unchecked_mul
4+
|
5+
| #[kani::proof_for_contract(NonZero::<g32>::unchecked_mul)]
6+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7+
8+
Failed to resolve checking function NonZero::unchecked_mul because there are multiple implementations of unchecked_mul in struct `NonZero`. Found:
9+
NonZero::<u32>::unchecked_mul
10+
NonZero::<i32>::unchecked_mul
11+
|
12+
| #[kani::proof_for_contract(NonZero::unchecked_mul)]
13+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
14+
|
15+
= help: Replace NonZero::unchecked_mul with a specific implementation.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
// kani-flags: -Zfunction-contracts
4+
5+
// Test that Kani errors if the target of the proof_for_contract is missing a generic argument that's required to disambiguate between multiple implementations
6+
// or if the generic argument is invalid.
7+
// c.f. https://github.com/model-checking/kani/issues/3773
8+
9+
struct NonZero<T>(T);
10+
11+
impl NonZero<u32> {
12+
#[kani::requires(self.0.checked_mul(x).is_some())]
13+
fn unchecked_mul(self, x: u32) -> u32 {
14+
self.0 * x
15+
}
16+
}
17+
18+
impl NonZero<i32> {
19+
#[kani::requires(self.0.checked_mul(x).is_some())]
20+
fn unchecked_mul(self, x: i32) -> i32 {
21+
self.0 * x
22+
}
23+
}
24+
25+
// Errors because there is more than one unchecked_mul implementation
26+
#[kani::proof_for_contract(NonZero::unchecked_mul)]
27+
fn verify_unchecked_mul_ambiguous_path() {
28+
let x: NonZero<i32> = NonZero(-1);
29+
x.unchecked_mul(-2);
30+
}
31+
32+
// Errors because the g32 implementation doesn't exist
33+
#[kani::proof_for_contract(NonZero::<g32>::unchecked_mul)]
34+
fn verify_unchecked_mul_invalid_impl() {
35+
let x: NonZero<i32> = NonZero(-1);
36+
NonZero::<i32>::unchecked_mul(x, -2);
37+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
error: Failed to resolve replacement function NonZero::unchecked_mul: there are multiple implementations of unchecked_mul in struct `NonZero`. Found:
2+
NonZero::<u32>::unchecked_mul
3+
NonZero::<i32>::unchecked_mul
4+
invalid_inherent_impls.rs
5+
|
6+
| #[kani::stub_verified(NonZero::unchecked_mul)]
7+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8+
|
9+
= help: Replace NonZero::unchecked_mul with a specific implementation.
10+
11+
error: Failed to resolve replacement function NonZero::<g32>::unchecked_mul: the generic arguments ::<g32> are invalid. The available implementations are:
12+
NonZero::<u32>::unchecked_mul
13+
NonZero::<i32>::unchecked_mul
14+
invalid_inherent_impls.rs
15+
|
16+
| #[kani::stub_verified(NonZero::<g32>::unchecked_mul)]
17+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
18+
|
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
//
4+
// kani-flags: --harness invalid_stub -Z stubbing
5+
6+
// Test that Kani errors if the stub is missing a generic argument that's required to disambiguate between multiple implementations
7+
// or if the generic argument is invalid.
8+
// c.f. https://github.com/model-checking/kani/issues/3773
9+
10+
struct NonZero<T>(T);
11+
12+
impl NonZero<u32> {
13+
#[kani::requires(self.0.checked_mul(x).is_some())]
14+
fn unchecked_mul(self, x: u32) -> u32 {
15+
self.0 * x
16+
}
17+
}
18+
19+
impl NonZero<i32> {
20+
#[kani::requires(self.0.checked_mul(x).is_some())]
21+
fn unchecked_mul(self, x: i32) -> i32 {
22+
self.0 * x
23+
}
24+
}
25+
26+
// Errors because there is more than one unchecked_mul implementation
27+
#[kani::stub_verified(NonZero::unchecked_mul)]
28+
#[kani::proof]
29+
fn verify_unchecked_mul_ambiguous_path() {
30+
let x: NonZero<i32> = NonZero(-1);
31+
x.unchecked_mul(-2);
32+
}
33+
34+
// Errors because the g32 implementation doesn't exist
35+
#[kani::stub_verified(NonZero::<g32>::unchecked_mul)]
36+
#[kani::proof]
37+
fn verify_unchecked_mul_invalid_impl() {
38+
let x: NonZero<i32> = NonZero(-1);
39+
NonZero::<i32>::unchecked_mul(x, -2);
40+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
error: failed to resolve `crate::mod_a::method_a::invalid`: expected module, found function `mod_a::method_a`\
2-
invalid.rs:\
2+
invalid_mod.rs:\
33
|\
44
| #[cfg_attr(kani, kani::stub(crate::mod_a::method_a::invalid, noop))]\
File renamed without changes.

0 commit comments

Comments
 (0)