Skip to content

Commit 6fa4467

Browse files
authored
Support stubbing trait method implementations (#4587)
Enable stubbing of trait method implementations using fully-qualified syntax (`<Type as Trait>::method`), including generic traits. Previously, Kani's stub resolution only searched inherent `impl` blocks and trait method implementations were silently ignored. Resolves #1997. ## Problem Users could not stub trait method implementations. Writing `#[kani::stub(<MyType as MyTrait>::method, mock_method)]` failed because the resolution algorithm did not search through trait implementations for the concrete type. This was a significant limitation since many Rust APIs are trait-based. ## Solution ### Resolution (`resolve.rs`) `resolve_in_trait_impl` now correctly resolves trait method implementations by: 1. Extracting generic arguments from the trait segment (e.g., `<u32>` from `Convert<u32>`) 2. Building the full `GenericArgs` with the Self type + trait generic parameters 3. Resolving the `Instance` for the concrete type's implementation This handles both simple traits (`<Vec<u8> as MyTrait>::method`) and generic traits (`<MyType as Convert<u32>>::convert`). ### Documentation (`stubbing.md`) Updated the user-facing docs to document trait method stubbing with examples covering: - Basic trait method stubbing syntax - Generic trait methods - Supported patterns (dyn dispatch, supertraits, overridden defaults) - Known limitation: non-overridden default methods ## Testing **5 passing tests:** - `stub_trait_method.rs` — basic `<Type as Trait>::method` stubbing - `stub_generic_trait_method.rs` — generic trait `<Type as Trait<T>>::method` - `stub_trait_dyn_dispatch.rs` — dynamic dispatch through `&dyn Trait` and `Box<dyn Trait>` - `stub_trait_supertrait.rs` — methods defined in supertraits - `stub_trait_default_overridden.rs` — default trait methods that are overridden in the impl **1 fixme test:** - `fixme_stub_trait_default_method.rs` — non-overridden default methods cause a type mismatch during stub validation (the default body uses `Self` as a placeholder type) --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses. --------- Signed-off-by: Felipe R. Monteiro <felisous@amazon.com>
1 parent 78eb465 commit 6fa4467

14 files changed

Lines changed: 432 additions & 32 deletions

docs/src/reference/experimental/stubbing.md

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,47 @@ In the following, we describe the known limitations of the stubbing feature.
170170

171171
### Trait method stubbing
172172

173-
Stubbing trait method implementations (e.g., `<MyType as MyTrait>::method`) is **not supported**.
174-
Only free functions and inherent methods can be stubbed. Rust's attribute path syntax does not
175-
support the fully-qualified `<Type as Trait>::method` form, and the resolution algorithm does not
176-
search through trait implementations.
173+
Kani supports stubbing trait method implementations using fully-qualified syntax:
177174

178-
If you need to stub a trait method, consider stubbing the calling function instead, or using
179-
conditional compilation (`#[cfg(kani)]`) to provide an alternative implementation.
175+
```rust
176+
trait MyTrait {
177+
fn method(&self) -> u32;
178+
}
179+
180+
struct MyType;
181+
182+
impl MyTrait for MyType {
183+
fn method(&self) -> u32 { 0 }
184+
}
185+
186+
fn stub_method(_x: &MyType) -> u32 { 42 }
187+
188+
#[kani::proof]
189+
#[kani::stub(<MyType as MyTrait>::method, stub_method)]
190+
fn check_trait_stub() {
191+
let x = MyType;
192+
assert_eq!(x.method(), 42);
193+
}
194+
```
195+
196+
This also works with generic traits:
197+
198+
```rust
199+
#[kani::stub(<MyType as Convert<u32>>::convert, stub_convert)]
200+
```
201+
202+
When two traits define methods with the same name, the fully-qualified syntax
203+
disambiguates which implementation to stub.
204+
205+
The following trait patterns are supported:
206+
207+
- **Supertrait methods:** Stubbing a method defined in a supertrait (e.g., `<MyType as Base>::method`) works independently of subtrait methods.
208+
- **Overridden default methods:** If a type overrides a trait's default method, the override can be stubbed normally.
209+
- **Dynamic dispatch:** Stubs apply even when the method is called through a trait object (`&dyn Trait` or `Box<dyn Trait>`).
210+
211+
**Known limitation:** Stubbing a trait's default method that is *not* overridden by the implementing type is not currently supported ([#4588](https://github.com/model-checking/kani/issues/4588)). The default method body uses `Self` as a placeholder type, which causes a type mismatch during stub validation. This applies only to default methods that are inherited as-is (i.e., the `impl` block does not provide its own definition).
212+
213+
**Known limitation:** Traits with const generic parameters (e.g., `<Type as Buf<16>>::write`) or associated type constraints (e.g., `<Type as Iterator<Item = u32>>::next`) are not currently supported and will produce a resolution error.
180214

181215
### Usage restrictions
182216

@@ -191,7 +225,6 @@ Support for stubbing is currently **limited to functions and methods**. All othe
191225
The following are examples of items that could be good candidates for stubbing, but aren't supported:
192226
- Types
193227
- Macros
194-
- Traits
195228
- Intrinsics
196229

197230
We acknowledge that support for method stubbing isn't as ergonomic as it could be.

kani-compiler/src/kani_middle/resolve.rs

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,24 @@ pub fn resolve_fn_path<'tcx>(
7373
let ty = type_resolution::resolve_ty(tcx, current_module, syn_ty)?;
7474
let trait_fn_id = resolve_path(tcx, current_module, &path.path)?;
7575
validate_kind!(tcx, trait_fn_id, "function / method", DefKind::Fn | DefKind::AssocFn)?;
76-
resolve_in_trait_impl(tcx, ty, trait_fn_id)
76+
// Extract generic args from the trait segment (e.g., <u32> from Convert<u32>).
77+
// For `<Type as crate::mod::Trait<u32>>::method`, segments after `as` are
78+
// [crate, mod, Trait<u32>, method]. The trait segment with generic args is
79+
// always second-to-last (last is the method name).
80+
let trait_generic_args = path
81+
.path
82+
.segments
83+
.iter()
84+
.rev()
85+
.nth(1) // The trait segment is second-to-last (last is the method name)
86+
.and_then(|seg| {
87+
if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
88+
Some(args)
89+
} else {
90+
None
91+
}
92+
});
93+
resolve_in_trait_impl(tcx, Some(current_module), ty, trait_fn_id, trait_generic_args)
7794
}
7895
// Qualified path for a primitive type, such as `<[u8]::sort>`.
7996
Some(QSelf { ty: syn_ty, .. }) if type_resolution::is_type_primitive(syn_ty) => {
@@ -399,8 +416,17 @@ fn resolve_in_any_trait<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty, name: &str) -> Option<F
399416
.into_iter()
400417
.filter_map(|trait_def| {
401418
resolve_in_trait_def_stable(tcx, trait_def, name).ok().and_then(|item| {
402-
resolve_in_trait_impl(tcx, ty, rustc_internal::internal(tcx, item.def_id.def_id()))
403-
.ok()
419+
// No current_module or trait_args needed: this path resolves
420+
// unqualified method names (e.g., `x.foo()`), not user-specified
421+
// `<Type as Trait<T>>::method` syntax.
422+
resolve_in_trait_impl(
423+
tcx,
424+
None,
425+
ty,
426+
rustc_internal::internal(tcx, item.def_id.def_id()),
427+
None,
428+
)
429+
.ok()
404430
})
405431
})
406432
.collect();
@@ -411,16 +437,49 @@ fn resolve_in_any_trait<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty, name: &str) -> Option<F
411437
/// Resolves a trait method implementation by checking if there exists an Instance of the trait method for `ty`.
412438
/// This is distinct from `resolve_in_trait_def`: that function checks if the associated function is defined for the trait,
413439
/// while this function checks if it's implemented for `ty`.
414-
fn resolve_in_trait_impl(
415-
tcx: TyCtxt<'_>,
440+
fn resolve_in_trait_impl<'tcx>(
441+
tcx: TyCtxt<'tcx>,
442+
current_module: Option<LocalDefId>,
416443
ty: Ty,
417444
trait_fn_id: DefId,
418-
) -> Result<FnResolution, ResolveError<'_>> {
419-
debug!(?ty, "resolve_in_trait_impl");
445+
trait_args: Option<&syn::AngleBracketedGenericArguments>,
446+
) -> Result<FnResolution, ResolveError<'tcx>> {
447+
debug!(?ty, ?trait_args, "resolve_in_trait_impl");
420448
// Given the `FnDef` of the *definition* of the trait method, see if there exists an Instance
421449
// that implements that method for `ty`.
422450
let trait_fn_fn_def = stable_fn_def(tcx, trait_fn_id).unwrap();
423-
let desired_generic_args = GenericArgs(vec![GenericArgKind::Type(ty)]);
451+
452+
// Build generic args: Self type + any trait generic parameters (e.g., T in Convert<T>).
453+
let mut args = vec![GenericArgKind::Type(ty)];
454+
if let Some(syn_args) = trait_args {
455+
let Some(module) = current_module else {
456+
return Err(ResolveError::UnsupportedPath {
457+
kind: "stub resolution requires generic trait arguments to have a known module context",
458+
});
459+
};
460+
for arg in &syn_args.args {
461+
match arg {
462+
syn::GenericArgument::Type(syn_ty) => {
463+
let resolved_ty = type_resolution::resolve_ty(tcx, module, syn_ty)?;
464+
args.push(GenericArgKind::Type(resolved_ty));
465+
}
466+
syn::GenericArgument::Const(_) => {
467+
return Err(ResolveError::UnsupportedPath {
468+
kind: "const generic arguments in trait stubs",
469+
});
470+
}
471+
syn::GenericArgument::Lifetime(_) => {
472+
// Lifetimes are erased — safe to skip.
473+
}
474+
_ => {
475+
return Err(ResolveError::UnsupportedPath {
476+
kind: "associated type/const constraints in trait stubs",
477+
});
478+
}
479+
}
480+
}
481+
}
482+
let desired_generic_args = GenericArgs(args);
424483
let exists = Instance::resolve(trait_fn_fn_def, &desired_generic_args);
425484

426485
// If such an Instance exists, return *its* FnDef (i.e., the FnDef inside the impl block for this `ty`)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
//
4+
// kani-flags: -Z stubbing
5+
//
6+
//! Test stubbing a trait's default method implementation.
7+
//!
8+
//! FIXME: Default trait methods use `Self` in their body types, which
9+
//! doesn't match the concrete type in the stub. The validation compares
10+
//! the trait definition's body (with `&Self`) against the stub (with
11+
//! `&MyType`), causing a false type mismatch.
12+
//! Tracked in: https://github.com/model-checking/kani/issues/4588
13+
14+
trait HasDefault {
15+
fn default_method(&self) -> u32 {
16+
0
17+
}
18+
}
19+
20+
struct MyType;
21+
impl HasDefault for MyType {}
22+
23+
fn stub_default(_x: &MyType) -> u32 {
24+
42
25+
}
26+
27+
#[kani::proof]
28+
#[kani::stub(<MyType as HasDefault>::default_method, stub_default)]
29+
fn check_stub_default_trait_method() {
30+
let x = MyType;
31+
assert_eq!(x.default_method(), 42);
32+
}

tests/ui/function-contracts/generic_slice_index.rs renamed to tests/kani/Stubbing/stub_generic_slice_index.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
// Copyright Kani Contributors
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
33
// kani-flags: -Zfunction-contracts
4-
//! Test function contracts on generic trait implementations based on SliceIndex,
5-
//! c.f. https://github.com/model-checking/kani/issues/4084
6-
//! This `proof_for_contract` should work,
7-
//! but we do not yet support stubbing/contracts on trait fns with generic arguments
8-
//! c.f. https://github.com/model-checking/kani/issues/1997#issuecomment-3134614734.
9-
//! So for now, test that we emit a nice error message.
4+
//! Test function contracts on generic trait implementations based on SliceIndex.
5+
//! Regression test for https://github.com/model-checking/kani/issues/1997
6+
//! and https://github.com/model-checking/kani/issues/4084.
107
118
const INVALID_INDEX: usize = 10;
129

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
//
4+
// kani-flags: -Z stubbing
5+
//
6+
//! Test stubbing generic trait method implementations.
7+
//! Regression test for https://github.com/model-checking/kani/issues/1997
8+
9+
trait Convert<T> {
10+
fn convert(&self) -> T;
11+
}
12+
13+
struct MyType;
14+
15+
impl Convert<u32> for MyType {
16+
fn convert(&self) -> u32 {
17+
100
18+
}
19+
}
20+
21+
impl Convert<bool> for MyType {
22+
fn convert(&self) -> bool {
23+
false
24+
}
25+
}
26+
27+
fn stub_convert_u32(_x: &MyType) -> u32 {
28+
42
29+
}
30+
31+
#[kani::proof]
32+
#[kani::stub(<MyType as Convert<u32>>::convert, stub_convert_u32)]
33+
fn check_generic_trait_stub() {
34+
let m = MyType;
35+
assert_eq!(<MyType as Convert<u32>>::convert(&m), 42);
36+
// The bool impl should NOT be affected
37+
assert_eq!(<MyType as Convert<bool>>::convert(&m), false);
38+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
//
4+
// kani-flags: -Z stubbing
5+
//
6+
//! Test stubbing a trait default method that IS overridden in the impl.
7+
//! The stub should replace the overridden implementation, not the default.
8+
9+
trait HasDefault {
10+
fn method(&self) -> u32 {
11+
0
12+
}
13+
}
14+
15+
struct MyType;
16+
17+
impl HasDefault for MyType {
18+
fn method(&self) -> u32 {
19+
100
20+
}
21+
}
22+
23+
fn stub_method(_x: &MyType) -> u32 {
24+
42
25+
}
26+
27+
#[kani::proof]
28+
#[kani::stub(<MyType as HasDefault>::method, stub_method)]
29+
fn check_stub_overridden_default() {
30+
let x = MyType;
31+
assert_eq!(x.method(), 42);
32+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
//
4+
// kani-flags: -Z stubbing
5+
//
6+
//! Test stubbing a trait method called through dynamic dispatch (trait objects).
7+
8+
trait Animal {
9+
fn sound(&self) -> u32;
10+
}
11+
12+
struct Dog;
13+
14+
impl Animal for Dog {
15+
fn sound(&self) -> u32 {
16+
100
17+
}
18+
}
19+
20+
fn stub_sound(_x: &Dog) -> u32 {
21+
42
22+
}
23+
24+
fn call_via_dyn(a: &dyn Animal) -> u32 {
25+
a.sound()
26+
}
27+
28+
#[kani::proof]
29+
#[kani::stub(<Dog as Animal>::sound, stub_sound)]
30+
fn check_stub_dyn_dispatch() {
31+
let dog = Dog;
32+
// Direct call — should be stubbed
33+
assert_eq!(dog.sound(), 42);
34+
// Call through trait object — stub applies because Kani replaces the
35+
// function body globally, which is used regardless of dispatch mechanism.
36+
let result = call_via_dyn(&dog);
37+
assert_eq!(result, 42);
38+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
//
4+
// kani-flags: -Z stubbing
5+
//
6+
//! Test stubbing trait method implementations using fully-qualified syntax.
7+
//! Regression test for https://github.com/model-checking/kani/issues/1997
8+
9+
trait A {
10+
fn foo(&self) -> u32;
11+
fn bar(&self) -> u32;
12+
}
13+
14+
trait B {
15+
fn bar(&self) -> u32;
16+
}
17+
18+
struct X {}
19+
20+
impl X {
21+
fn new() -> Self {
22+
Self {}
23+
}
24+
}
25+
26+
impl A for X {
27+
fn foo(&self) -> u32 {
28+
100
29+
}
30+
fn bar(&self) -> u32 {
31+
200
32+
}
33+
}
34+
35+
impl B for X {
36+
fn bar(&self) -> u32 {
37+
300
38+
}
39+
}
40+
41+
fn stub_1(_x: &X) -> u32 {
42+
1
43+
}
44+
fn stub_2(_x: &X) -> u32 {
45+
2
46+
}
47+
fn stub_3(_x: &X) -> u32 {
48+
3
49+
}
50+
51+
#[kani::proof]
52+
#[kani::stub(<X as A>::foo, stub_1)]
53+
fn check_stub_trait_a_foo() {
54+
let x = X::new();
55+
assert_eq!(x.foo(), 1);
56+
}
57+
58+
#[kani::proof]
59+
#[kani::stub(<X as A>::bar, stub_2)]
60+
fn check_stub_trait_a_bar() {
61+
let x = X::new();
62+
assert_eq!(A::bar(&x), 2);
63+
}
64+
65+
#[kani::proof]
66+
#[kani::stub(<X as B>::bar, stub_3)]
67+
fn check_stub_trait_b_bar() {
68+
let x = X::new();
69+
assert_eq!(<X as B>::bar(&x), 3);
70+
}

0 commit comments

Comments
 (0)