Follow up to "Changing the rules of Rust"

In my previous post, I described the idea of using an edition mechanism to introduce a new auto trait. I wrote that the compiler would need to create an “unbreakable firewall” to prevent using !Leak types from the new edition with code from the old edition that assumes values of all types can be leaked.

The response has been pretty optimistic that ensuring this would be possible, even though I wrote in the post myself that I “despair” over how difficult it was. I’ve received a great example from Ariel Ben-Yehuda which demonstrates how this problem is more difficult to solve than you would probably think.

This example is going to involve four different crates. The first crate, foo, is in the 2024 edition, and defines a trait with a single generic method:

// crate foo; edition = 2024

trait Foo {
    fn foo<T>(input: T);
}

The second crate, bar, is in the 2021 edition, depends on foo, and implements that trait for its own type. Its implementation forgets the argument to that method:

// crate bar; edition = 2021

pub struct Bar;

impl foo::Foo for Bar {
    fn foo<T>(input: T) {
        std::mem::forget(input);
    }
}

This code is legal under the firewall rules I listed last time!

The third crate, baz, is in the 2024 edition. It only depends on foo, not bar, and uses the trait in a new function, using its own private !Leak type:

// crate baz; edition = 2024

struct Baz;

impl !Leak for Baz { }

pub fn baz<T: foo::Foo>() {
    T::foo(Baz);
}

All of the code in foo and baz is in the 2024 edition, and all of it is valid, because none of it involves a Leak bound or any 2021 edition code. Maybe you can see where this is going..

A fourth crate, quux, depends on both bar and baz:

// crate quux;

pub fn quux() {
    baz::baz::<bar::Bar>()
}

This code now leaks a type that doesn’t implement Leak! baz::baz will pass its !Leak type to bar::Bar::foo, which will leak it. Somewhere, there must be an error. But where?

Before you start typing your response, consider:

  • I avoided saying what edition quux is compiling under. It does not matter. If all of foo, bar, and baz compiled, it must be quux which doesn’t compile, regardless of what edition it’s compiled under.
  • The error cannot be in foo, because because surely traits with generic methods are not an error and will not become an error in 2024.
  • The error cannot be in baz, because nothing in baz is invalid at all: you instantiated a generic interface with a type that meets the bounds of that interface.
  • Upgrading your crate to a new edition has historically not been considered a breaking change, except insofar as it raises your MSRV. This is very important for achieving a seamless upgrade across editions, as everyone will not upgrade at once. If you say the error is in bar, for example, the implication is that upgrading foo from 2021 to 2024 is a breaking change, because you can implement both foo and bar exactly as written under edition 2021 today. So the error cannot be in bar.

It seems like the error just to be in quux, but none of the types involved in quux have anything at all to do with Leak. What are the rules you are enforcing to make quux error? And how do you explain them to users so that they have half a clue what’s going on when they run into this issue in a practical scenario? Good luck!