Skip to content
This repository was archived by the owner on Apr 5, 2024. It is now read-only.

Check for variance changes #364

@mstange

Description

@mstange

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, and union 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions