-
Notifications
You must be signed in to change notification settings - Fork 41
Check for variance changes #364
Description
I just learned about variance in Rust, and was struck by its potential to cause unintended API breakages.
Does this tool check for changes in a type's variance over its lifetime and type parameters?
For example, a generic struct of the form TheStruct<T>
can go from being "covariant in T
" to being "invariant in T
" simply because a hidden field was added to the struct. Same with lifetime parameters: AnotherStruct<'a>
can go from being "covariant in 'a
" to being "invariant in 'a
".
From https://doc.rust-lang.org/reference/subtyping.html:
The variance of other
struct
,enum
, andunion
types is decided by looking at the variance of the types of their fields.
Variance is an aspect of a type's API that is not immediately obvious, yet it has the potential to break existing code when it changes.
Here's an example where crate_1_0_0::TheStruct
is covariant in T
, which allows the function cast_return_value_1_0_0
to compile successfully. But in crate_1_0_1
, TheStruct
now has a Mutex
field which causes TheStruct
to become invariant in T
, and as a result, cast_return_value_1_0_1
no longer compiles.
mod crate_1_0_0 {
pub struct TheStruct<T> {
inner: T,
}
impl<T> TheStruct<T> {
pub fn new(inner: T) -> Self {
Self { inner }
}
pub fn into_inner(self) -> T {
self.inner
}
}
}
mod crate_1_0_1 {
pub struct TheStruct<T> {
inner: std::sync::Mutex<T>,
}
impl<T> TheStruct<T> {
pub fn new(inner: T) -> Self {
Self {
inner: std::sync::Mutex::new(inner),
}
}
pub fn into_inner(self) -> T {
self.inner.into_inner().unwrap()
}
}
}
// Compiles successfully
fn cast_return_value_1_0_0<'a>(
x: crate_1_0_0::TheStruct<&'a str>,
y: crate_1_0_0::TheStruct<&'static str>,
) -> crate_1_0_0::TheStruct<&'a str> {
println!("Discarding: {}", x.into_inner());
y
}
// Does not compile
fn cast_return_value_1_0_1<'a>(
x: crate_1_0_1::TheStruct<&'a str>,
y: crate_1_0_1::TheStruct<&'static str>,
) -> crate_1_0_1::TheStruct<&'a str> {
println!("Discarding: {}", x.into_inner());
y
}
fn main() {
let s = String::from("Short-lived string");
let struct_short_lived = crate_1_0_0::TheStruct::new(s.as_str());
let struct_static = crate_1_0_0::TheStruct::new("Static string");
let one = cast_return_value_1_0_0(struct_short_lived, struct_static);
println!("Remaining: {}", one.into_inner());
let s = String::from("Short-lived string");
let struct_short_lived = crate_1_0_1::TheStruct::new(s.as_str());
let struct_static = crate_1_0_1::TheStruct::new("Static string");
let one = cast_return_value_1_0_1(struct_short_lived, struct_static);
println!("Remaining: {}", one.into_inner());
}
This prints:
error: lifetime may not live long enough
--> src/main.rs:46:5
|
41 | fn cast_return_value_1_0_1<'a>(
| -- lifetime `'a` defined here
...
46 | y
| ^ returning this value requires that `'a` must outlive `'static`
|
= note: requirement occurs because of the type `crate_1_0_1::TheStruct<&str>`, which makes the generic argument `&str` invariant
= note: the struct `crate_1_0_1::TheStruct<T>` is invariant over the parameter `T`
= help: see <https://doc.rust-lang.org/nomicon/subtyping.html> for more information about variance