Skip to content

Instantly share code, notes, and snippets.

@danielhenrymantilla
Last active July 28, 2025 07:26
Show Gist options
  • Save danielhenrymantilla/b9fa3718841a38ef297596038974fbbd to your computer and use it in GitHub Desktop.
Save danielhenrymantilla/b9fa3718841a38ef297596038974fbbd to your computer and use it in GitHub Desktop.
The curious case of the `rustfmt`-induced lifetime error

The curious case of the rustfmt-induced lifetime error

Consider the following snippet, which compiles fine:

trait Tr {}

struct A(Option<Box<dyn Tr>>);

impl A {
    fn f(&mut self) -> Option<&mut dyn Tr> {
        self.0.as_mut().map(|x| { &mut **x })
    }
}

Now, say, you run rustfmt on it, which sees the "trivial closure", and deems the theoretically-non-semantically-meaningful braces to be unnecessary, readability-wise, so it yeets them:

//! After `rustfmt`:

trait Tr {}
struct A(Option<Box<dyn Tr>>);
impl A {
    fn f(&mut self) -> Option<&mut dyn Tr> {
        self.0.as_mut().map(|x| &mut **x)
        //                     ^        ^
        //                      no braces
    }
}

The baffling situation here is that this snippet now fails to compile!

error: lifetime may not live long enough
 --> <source>:9:33
  |
8 |     fn f(&mut self) -> Option<&mut dyn Tr> {
  |          - let's call the lifetime of this reference `'1`
9 |         self.0.as_mut().map(|x| &mut **x)
  |                                 ^^^^^^^^ returning this value requires that `'1` must outlive `'static`
  |
help: to declare that the trait object captures data from argument `self`, you can add an explicit `'_` lifetime bound
  |
8 |     fn f(&mut self) -> Option<&mut dyn Tr + '_> {
  |                                           ++++
help: consider adding 'move' keyword before the nested closure
  |
9 |         self.0.as_mut().map(move |x| &mut **x)
  |                             ++++

Which begs the natural question:

What the heck is going on!???

In order to answer it, we will need to properly stare at this snippet, scrutinizing every detail about lifetimes, and requiring a detailed, rigorous, exploration of Rust mechanics.

For starters, let's throw the lifetimes into the spotlight.

Manually applying the "lifetime elision" rules, to unelide them and show them

struct A(Option<Box<dyn Tr>>);

impl A {
    fn f(&mut self) -> Option<&mut dyn Tr> {

1. "Lifetime elision" rules for {F,f}n signatures

  1. Exhibit the implicitly-elided lifetimes as "holes"/placeholders / explicitly-elided lifetimes:

    struct A(Option<Box<dyn Tr>>);
    
    impl A {
        //    vv                      vv
        fn f(&'_ mut self) -> Option<&'_ mut dyn Tr> {
  2. For each such "hole"/placeholder/explicitly-elided lifetime in input position, introduce a separate new/fresh generic lifetime parameter to the function, and assign it to it:

    struct A(Option<Box<dyn Tr>>);
    
    impl A {
        //      0-th input `'_`
        //         vv           —and that's it.
        fn f<...>(&'_ mut self) -> Option<&'_ mut dyn Tr> {
        //                                 ^^
        //                             not in input.

    which yields:

    struct A(Option<Box<dyn Tr>>);
    
    impl A {
        //                               not in input.
        //         vvv                      vv
        fn f<'_0>(&'_0 mut self) -> Option<&'_ mut dyn Tr> {
  3. For every "hole"/placeholder/explicitly-elided lifetime in output position, assign The One Input Lifetime™ (to all of them).

    Where The One Input Lifetime™ is defined as:

    • if there is &'x [mut] self among the fn args, then this 'x lifetime of the borrow (mut or not) in self.
    • otherwise, if there is exactly one named lifetime1 among all the fn args, then that one lifetime.
    struct A(Option<Box<dyn Tr>>);
    
    impl A {
        //                               The One Input Lifetime™
        //         vvv                      vvv
        fn f<'_0>(&'_0 mut self) -> Option<&'_0 mut dyn Tr> {

    And voilà.

But we are not done!

Indeed, there is another, distinct, lifetime elision mechanism in Rust, which is at play here:

2. "Lifetime elision" rules inside dyn Traits

The type dyn Bounds… is only complete/well-defined if there is exactly2 one explicit + 'usability bound

For the sake of ergonomics & convenience, such + 'usability can, however, be elided. But it is still there!

Let's use, in pseudo-code, the '^? sigil to represent such elided lifetimes.

  • ? to signify that it is something to be resolved there and then, independently of other '^ occurrences;
  • ^ as an arbitrary, non-_ sigil, picked as a subtle reminder of this 'usability lifetime being, in practice, the intersection of every lifetime param occurring in the original, before-dyn-erasure, type (or something even smaller than that / contained within it).

In our snippet, we have two such occurrences:

struct A(Option<Box<dyn '^? + Tr>>);

impl A {
    //                               The One Input Lifetime™
    //         vvv                      vvv
    fn f<'_0>(&'_0 mut self) -> Option<&'_0 mut (dyn '^? + Tr)> {

What the "lifetime elision" rules inside dyn Traits stipulate, is the following:

  • inside { … } expression blocks (such as, mainly, fn bodies, but also, const … = { … }; bodies), '^? is inferred by the compiler to whatever could make your code compile.

    In other words, within an { … } expression block, manually making the + 'usability explicit shall not be able to make more code compile (except for the limits of inference vs. explicit annotations: coërcions…).

  • Otherwise / outside of such expression contexts, the '^? cannot be inferred, and is instead determined by strict rules:

    1. it defaults to 'static;

    2. but can become 'lt whenever the dyn … type sits in a position with "an explicit : 'lt constraint" on it.

      To illustrate, there are mainly two occurrences of this "explicit : 'lt constraint" on a type:

      • either spelled out explicitly, e.g., struct Foo<'lt, T : ?Sized + 'lt>, or trait Bar<'lt, T : ?Sized + 'lt>, or even: type Alias<'lt, T : 'lt>.

        In these, when feeding dyn Trait… (with no explicit + 'usability) as the <T> type parameter, the '^? shall be equal to 'lt.

        Foo<'lt, dyn Trait /* + 'lt */>
        dyn Bar<'lt, dyn Trait /* + 'lt */>
        impl Bar<'lt, dyn Trait /* + 'lt */>
        Alias<'lt, dyn Trait /* + 'lt */>
      • or implicitly, stemming from Well-Formedness (WF) rules around &'r [mut] T, which stipulate that T : 'r.

        &'r [mut] (dyn Trait /* + 'r */)

      So, and this is the most notable example: &'r [mut] (dyn Bounds…), when the Bounds… carry no explicit + 'usability, is sugar for &'r [mut] (dyn 'r + Bounds…)

Applying these rules, we get:

  • For struct A(Option<Box<dyn '^? + Tr>>);, this is case 1., and '^? is 'static

  • For &'_0 mut (dyn '^? + Tr), this is case 2. (implicit : '_0 constraint on that dyn), and thus '^? is '_0.

Thus:

struct A(Option<Box<dyn 'static + Tr>>);

impl A {
    //                               The One Input Lifetime™
    //         vvv                      vvv
    fn f<'_0>(&'_0 mut self) -> Option<&'_0 mut (dyn '_0 + Tr)> {

Let's rename the generic lifetime param, to, e.g., the lifetime of the 'reference in self:

struct A(Option<Box<dyn 'static + Tr>>);

impl A {
    fn f<'r>(&'r mut self) -> Option<&'r mut (dyn 'r + Tr)> {

By the way, using the explicitly-elided lifetime/placeholder '_ as the + 'usability lifetime is possible,

Click here to know more about this

and, as far as the dyn Bounds… is concerned, this does count as an explicit + 'usability constraint, thereby disabling the "lifetime elision" rules for dyn. With that being said, '_ is only allowed:

  • in { … } expresion bodies, where it shall signify "infer this", and thus, it shall match exactly the same semantics as '^? (in that context);

  • in fn signatures, where it shall act as an elided lifetime w.r.t. the "lifetime elision" rules of {F,f}n signatures. These are (potentially) distinct semantics from those of '^?

    An example:

    Click to see
    fn demo(s: &str) -> Box<dyn Display> {
        Box::new(s) // error, `s` does not live long enough
    }
    // indeed, this is the same as:
    fn demo_explicitly_elided(s: &'_ str) -> Box<dyn '^? + Display> {}
    // i.e.
    fn demo_unelided<'s>(s: &'s str) -> Box<dyn 'static + Display> {
        Box::new(s) // error, `&'s str : 'static + Display` is not satisfied.
    }
  • in impl-block position, where it shall act "as fn-input '_", sort to speak. That is, it shall make the impl<…> block generic over an extra hidden lifetime parameter, impl<…, 'hidden_N>, and amount to dyn 'hidden_N + Trait.

    An interesting example here is the following snippet:

    Click to see
    trait Trait {}
    
    // yes, it is possible to add _inherent_ functionality to a given `dyn Trait`
    // type whenever you are in the same crate which defined the trait!
    impl /* for */ dyn Trait {
        fn method_by_ref(&self) {}
    }
    
    fn demo<'r>(r: &'r dyn Trait) {
        r.method_by_ref(); // Error, `'r` does not live long, required to be `'static`.
    }
    Click here to see more about this

    This one fails, perhaps surprisingly, because of the "weirdness" of the '^? rules:

    trait Trait {}
    
    impl /* for */ dyn '^? + Trait {
        fn method_by_ref(&self) {}
    }
    
    fn demo<'r>(r: &'r (dyn '^? + Trait)) {
        r.method_by_ref(); // Error, `'r` does not live long, required to be `'static`.
    }
    trait Trait {}
    
    //          unconstrained, thus, 'static.
    //                   v
    impl /* for */ dyn 'static + Trait {
        fn method_by_ref(&self) {}
    }
    
    //              `'r`-constrained, thus, 'r.
    //                       v
    fn demo<'r>(r: &'r (dyn 'r + Trait)) {
        r.method_by_ref(); // Error, `'r` does not live long, required to be `'static`.
     // ^
     // type here is `&'r (dyn 'r + Trait)`, needs to match
     // `&'? (dyn 'static + Trait)` as per the impl; which is only satisfiable
     // (via any one of coërcion or subtyping) when `'r : 'static`.
    }

    Note that the following snippet would still fail

    trait Trait {}
    
    impl /* for */ dyn Trait {
        fn method_by_ref(&self) {}
    }
    
    fn demo(r: &(dyn '_ + Trait)) {
        r.method_by_ref();
    }
    // since, when we unsugar this `demo`, we get:
    fn demo_unelided<'_0, '_1>(r: &'_0 (dyn '_1 + Trait)) {
        r.method_by_ref(); // Error, missing `'_1 : 'static`
    }

    But the following one would work:

    trait Trait {}
    
    impl /* for */ dyn '_ + Trait {
        fn method_by_ref(&self) {}
    }
    
    fn demo(r: &dyn Trait) {
        r.method_by_ref();
    }

    Indeed, we could unsugar it all down to:

    trait Trait {}
    
    impl<'u> /* for */ dyn 'u + Trait {
        fn method_by_ref<'method>(&'method self) {}
        fn method_by_ref<'method>(self: &'method Self) {}
        fn method_by_ref<'method>(self: &'method (dyn 'u + Trait)) {}
    }
    
    fn demo<'r>(r: &'r (dyn 'r + Trait)) {
        r.method_by_ref();
     // ^
     // a `&'r (dyn 'r + Trait)`: can we find two lifetimes `'u` and `'method` so that this type
     // "match" `&'method (dyn 'u + Trait)`? Yes, by picking `'u = 'method = 'r`, for instance.
     //   ^
     // (subtype or be coërcible to)
    }

Summary: our rustfmt-snippet, with the lifetimes all unelided

//! After `rustfmt`:

trait Tr {}

struct A(Option<Box<dyn 'static + Tr>>);

impl A {
    fn f<'r>(&'r mut self) -> Option<&'r mut (dyn 'r + Tr)> {
        //     inferred to be: `&'r mut Box<dyn 'static + Tr>`
        //                      |    remove the Box => `&'r mut (dyn 'static + Tr)`.
        //                      v       v
        self.0.as_mut().map(|x: _| &mut **x)
        //                        ^        ^
        //                        no braces
    }
}

So now we can finally spot the "lifetime mismatch", which can/could cause a compilation error:

Our &mut **x, at least initially, is of the type &'r mut (dyn 'static + Tr), but we expect to, eventually, have been dealing with an &'r mut (dyn 'r + Tr) inside our Option.

Can &'r mut (dyn 'static + Tr) "become"/"match" &'r mut (dyn 'r + Tr)?

In other words, can we somehow "shrink" that 'static in dyn 'static + … down to a smaller lifetime 'r, behind the outer &'r mut layer?

Through subtyping / (co)variance?

The first idea in these scenarios is to think of / reach for variance: the ability for certain types to simply be considered compatible with other types, for the sake of ergonomics, when such "lenience" from the compiler / type-checker is known/provable not to be able to cause problems, ever: when such "subtyping property" is said to be sound.

What is subtyping/variance?

  • T "subtypes" U is typically written as T <: U, and means that whenever the compiler encounters a value of type T, when a value of type U was expected instead, it will let it slide/pass / it won't error!

    fn example<'small>(
        s: &'static str
    ) -> &'small str
    {
        s // "Error, expected `&'small str`, got `&'static str`"?
          // Nope!
          // Nothing bad can happen from pessmistically considering our never-expiring
          // `&'static str` to actually be potentially expired beyond `'small`, so the compiler
          // might as well let this snippet compile.
    }
    
    fn other_example<'small>(
        s: Arc<Mutex<&'static str>>,
    ) -> Arc<Mutex<&'small str>>
    {
        s // "Error, expected `Arc<Mutex<&'small str>>`, got `Arc<Mutex<&'static str>>`?
          // Heck yeah!
          // Otherwise, somebody could take the returned mutex, and `*mutex.lock() = small_str;`.
          //
          // But whoever had given us this original `s: Arc<Mutex<&'static str>>` might still have
          // another clone of it around.
          //
          // So if they wait for `'small` to end, and `small_str` to _dangle_ / point to freed
          // memory, they would then be able to read garbage / freed memory (UAF), triggering UB,
          // when using that clone of theirs!
    }

    We can see in these examples that it can sometimes be sound (and convenient) to let lifetimes shrink, when othertimes, it wouldn't be sound to do so, so despite whatever inconvenience it entails, Rust cannot allow the lifetime shrinkage.

    The former case, where lifetimes are allowed to shrink, is called covariance (over that lifetime), and the latter, "lack of covariance":

    • in some edge cases, lifetimes may not be allowed to shrink, but they may be allowed to grow! (this ought to appear quite suprising, in a vacuum, I'll admit). Such cases are called contravariant.

    • but most often than not, stuff is either covariant, or neither-covariant-nor-contravariant, and there is a special term for the latter: invariance.

In Rust, there are only two forms of subtyping:

  • a "weird"/niche one, regarding for<'lt> fn…/dyn for<'lt> … and lack of for<>, which I won't talk about;

  • lifetime shrinkage-or-dilation. That is, given 'big : 'small, whether Stuff…'big… <: Stuff…'small… (or even whether Stuff…'small… <: Stuff…'big…).

    • Given a type Stuff…'lt…, if 'lt is "allowed to shrink" (i.e., if values of type Stuff…'small… are allowed to be used where Stuff…'lt… was expected, whenever 'lt : 'small), then Stuff…'lt… is said to be covariant (over 'lt).

    • else, if 'lt is "allowed to grow", Stuff…'lt… is said to be contravariant (over 'lt);

    • otherwise, it is said to be invariant (over 'lt).

    For wrapper types, such as Box<T> or Vec<T>, we can wonder how wrapping Stuff…'lt… in Wrapper<…> affects the variance of the resulting thing. Assuming Stuff…'lt… to be covariant:

    • if Wrapper<Stuff…'lt…> remains covariant, that is, Wrapper "maintained the covariance untouched/unscathed" / it let it "pass through", then Wrapper<T> is said to be (type-)covariant (over T).

    • else, if Wrapper<Stuff…'lt…> got its variance polarity "swapped/inverted", and becomes contravariant, then Wrapper<T> is said to be (type-)contravariant (over T).

    • otherwise, if Wrapper<Stuff…'lt…> loses its (co)variance, and becomes invariant, then Wrapper<T> is said to be (type-)invariant (over T).

Most important variance examples to have in mind:

  • the 'r lifetime of a 'reference, mut or not, is always allowed to shrink (it is sound to be unnecessarily pessimistic about an expiry date):

    no matter the type of the Referee3, &'r [mut] Referee is covariant over 'r (both in the &'r and &'r mut cases!).

    • ([mut] notation is precisely being used to denote that a property works both in the mut, and non-mut, cases.)
  • BUT, reference types themselves can be viewed under the prism of being a "Wrapper<>" around the Referee.

    Let's consider, for instance, Referee being &'s [mut] String (which, as per the first bullet, is covariant over 's).

    Then:

    • A shared ("immutable") reference is a (type-)covariant "wrapper" around its Referee.

      For instance, &'r &'s [mut] String is covariant over 's (and, as per the first bullet, over 'r)

    • An exclusive ("mutable") reference is a (type-)invariant "wrapper" around its Referee.

      For instance, &'r mut &'s [mut] String is invariant over 's (and, as per the first bullet, covariant over 'r).

    A good rule of thumb for these things could be:

    Outside of mut, covariant; but when inside/behind mut, invariant.

  • Other mutability type-wrappers, such as Cell<T>, RefCell<T>, Mutex<T>, RwLock<T>, are (type-)invariant (over T). Ditto for *mut T.

  • Smart pointers, such as Box and {A,Rc}, and most containers, such as Option<T>, [T], [T; N], Vec<T>, HashMap<T>, etc. are (type-)covariant (over T). Ditto fo *const T and ptr::NonNull<T>.

  • dyn 'usability + Bounds… is covariant over its + 'usability

  • dyn … + Trait<'generic, Generic> is invariant over 'generic lifetimes and Generic types (including associated types).

    And ditto for -> impl Trait<'generic, Generic>

  • <Type<…> as Trait<…>>::Assoc<…> is invariant over all three layers of <…>-generics.

  • The one and only case of contravariance: fn4 argument position:

    fn(…, Arg, …) is (type-)contravariant over Arg.

    Thus, for instance, fn(&'r str) is contravariant over 'r;

    Click here to get an intuition as to why

    Let's take an example: ID cards in real-life, acting as a &'expiry Id "reference" of sorts.

    Indeed, ID cards come with an inherent expiry date, to force renewal and whatnot.

    Funnily enough, at least in Spain, back in the day they would issue ID cards which would not expire!

    This is perfect for our analogy, since those will act as &'static Id "references".

    Now, we need something to act as a fn(&'expiry Id) type in our analogy.

    This is going to be some clerk office or whatnot, for paperwork.

    Consider a counter, with a clerk, Bob, to do some specific paperwork transaction, but on condition that the ID card not expire this year (lest Bob explode in a colourful explosion of red ribbons and tape).

    That is, we can consider them to work only with &'this_year Ids (or anything bigger: if somebody has an ID card which expires later, such as valid &'for_two_years, or even, valid &'forever i.e. &'static, then they can apply to this counter and be fine (this is by "covariance of &'expiry dates of references")).

    Now, to truly observe the contravariance at play, here, consider the following logistical requirement for that office: they also had another counter, with another clerk, Marley, doing the very same paperwork, but restricted to never-expiring IDs. And say that Marley called in sick or something (cough, smoking, cough).

    The million-dollar question is now: can the office ask Bob to take in Marley's stead/place? Can Bob, our instance of type fn(&'this_year Id), be "used in"/asked to work at a counter expecting Marleys, i.e., expecting fn(&'static Id)s?

    Well, the one meaningful restriction for Bob not to explode in red ribbons is for them not to run into an id that expires before the end of the year. But we know that at Marley's counter, all the ID cards/refs are everlasting.

    So no harm done, all is fine. That is:

    fn(&'this_year Id) <: fn(&'static Id)

  • Finally, for combinations of such types (such as field types among a given struct): they have a given variance (e.g., covariance) if and only if all of the types do.

    For simpler short-circuiting logic, it is thus preferable to consider lack of (a form of) variance as being infectious (much like a lack of an auto-trait such as Send is): the moment one type lacks it, any bigger type built atop it shall lack it as well, and so on, transitively.

    This is why I sometimes talk of covariance as "lack-of-contravariance", since that is its infectious (lack-of) variance property, and ditto for contravariance and "lack-of-covariance" (in Rust, you will NEVER have both covariance and contravariance simultaneously, by design (of the language authors)).

Back to our question

Does &'r mut (dyn 'static + Tr) subtype &'r mut (dyn 'r + Tr)?

That is, is &'r mut (dyn 'u + Tr) covariant-over-'u / allowed to have 'u shrink?

Well, dyn 'u + Tr is indeed covariant over 'u, but it happens to be "wrapped" behind mut, that is, inside the &'r mut _ wrapper type, which is a (type-)invariant wrapper. It "annihilates" any variance whatsoever occurring in _; here, the covariance-over-'u that we had. So the resulting type is invariant over 'u.

So no, 'u is not allowed to shrink in this type: the subtyping constraint it not satisfied.

Hence the error in the rustfmt-ed snippet.

But wait! If this were all to say about this snippet, how come the non-rustfmt-ed, braced-closure-body did compile fine?

Interlude: the curious case of &mut (dyn JaminButton + 'age)

  • (wherein 'age is spotted to go down!)

As a matter of fact, we could further reduce the successful compilation of the previous snippet down to the following one, which ought to have us agasp, since it will appear to be violating all of the principles of subtyping which I have just shown. As a matter of fact, and for what is worth, I do very distinctly remember having myself and a few other seasoned rustaceans of the URLO5 forum been quite puzzled by the "discovery":

pub trait JaminButton {}

pub fn the_curious_case<'r, 'u>(
    r: &'r mut (dyn JaminButton + 'static)
) -> &'r mut (dyn JaminButton + 'u)
{
    r // OK!??
}

If, generally, lifetimes behind &mut are to be invariant (not allowed to shrink now grow), how come in this snippet we are able to shrink the '_ lifetime parameter in &'r mut (dyn JaminButton + '_) from 'static down to 'u!?? This clearly appears to be covariant over 'u, which in the general case ought to be unsound!!

Surely this is an oversight from the compiler, and an unsoundness bug. Surely we will be able to come up with a snippet exploiting this unsoundness, right?

That's what we pondered about back in the day.

It turns out, the answer here is that this snippet is perfectly sound. In a very wave-handed manner, here are the key notions to get an intuition of what is going on:

  1. Why &mut T ought to be (type-)invariant over T:

    &mut T needs to be (type-)invariant over T since it is possible to write snippets using &mut T as a conceptual (Receiver<T>, Sender<T>) API:

    type SenderAndReceiver<'__, T> = &'__ mut T;
    
    fn send_and_receive<T>(
        channel: SenderAndReceiver<'_, T>,
        value_to_send: T,
    ) -> T // value received
    {
        let value_received: T =
            ::core::mem::replace::<T>(
                channel, // : &mut T,
                value_to_send, // : T
            ) // : T
        ;
        value_received
    }
    Click to see the rest of the explanation
    • Receiver<T> ~ (fn() -> T) ~ T is (type-)covariant over T (e.g., if we have T<'lt> being covariant over 'lt, then Receiver<T<'lt>> preserves that covariance over 'lt);

      • i.e., (type-)non-contravariant over T;
    • Sender<T> ~ fn(T) is (type-)contravariant over T (e.g., if we have T<'lt> being covariant over 'lt, then Sender<T<'lt>> swaps arounds/inverts that covariance, and we end up being contravariant over 'lt (i.e., 'lt being allowed to grow all the way up to 'static));

      • i.e., (type-)non-covariant over T.

    Thus, a (Receiver<T>, Sender<T>) combination thereof cannot be (type-)contravariant over T as per the former bullet, and cannot be (type-)covariant over T as per the latter bullet. It is thus to be (type-)invariant over T.

    QED.


  2. But if we stare at the above snippet, there is an important/crucial aspect to the Sender<T>-ness of &mut T: T was required to be Sized!!

    This is easy to overlook, since we are dealing with an implicit T : Sized bound stemming from the <T> verbatim introduction of that generic.

    It turns out that any attempt to "overwrite the referee T behind a &mut T" when T is not Sized is, in the general case, unsound.

    Click to see
    • We'd need to use unsafe to ever hope to implement such functionality;

    • We'd need to make sure not to mess up the size and alignement of the referee (since in the ?Sized realm, different instances can very much have different sizes or alignments);

    • At this point, the specific subcase of <T : ?Sized = [U]> would become sound to involve in a "&mut [U] can be used as a Sender<[U]> scenario;

    • But all the family of T = dyn 'u + Trait<…> types will still be unable to expose this soundly: you'd now need to ensure not only that size and alignment match, but also that that the original, concrete, underlying type of the old and new value do match as well. Which can only be done in a TypeId/Any kind of scenario, which, by design, is incompatible6 with any kind of non-'static lifetime parameter whatsoever (trait Any : 'static).

  3. So, if the Sender<T>-ness is no more in the !Sized case, so is the (type-)contravariance over T, i.e., the (type-)non-covariance over T.

    That is, it is no longer a problem to have &mut T when T : !Sized being (type-)covariant over T.

  4. So, there is no way to exploit our spotted instance of "lifetime shrinkage behind &mut", since our T<'lt> in question was a dyn 'lt + JaminButton, i.e., a very much non-Sized type!

Now the question is: does this mean that Rust subtyping is smart enough to allow this? Does it creäte exceptions to the General Rules of Variance™ (such as that of invariance when behind &mut) for cases where it knows it could do so soundly?

And the answer to this, as of July 2025, is:

a clear NO

Indeed, it is far from trivial for the compiler to always figure out these things: this could quickly spiral out of control w.r.t. cases which each user might require. Or at least this is my own, uneducated, guess on the question. Be it as it may, the compiler just doesn't do this (yet).

What the compiler does have, however, are mechanisms for "by-value"-specific "adjustments": type coërcions.

Through coërcions?

The very same mechanism which allows converting a PtrTo<[T; N]> (such as &[u8; 42]) into a PtrTo<[T]> (such as &[u8]), or a &[mut] String into a &[mut] str, or a bool into a u8, or a PtrTo<impl Trait> into a PtrTo<dyn Trait>, is also a mechanism able to convert a PtrTo<dyn 'a + Trait> into a PtrTo<dyn 'b + Trait> for certain choices of 'a and 'b:

coërcions word

The main one of interest, here, being the unsizing [to dyn] coërcion:

&'r mut (impl 'usability + Bounds)
// ↓ can be coërced into: ↓
&'r mut (dyn 'usability + Bounds)

also spelled out as:

pub trait Trait {}

pub fn unsizing_to_dyn_coercion<'r, 'u>(
    r: &'r mut (impl 'u + Trait)
) -> &'r mut (dyn 'u + Trait)
{
    r /* as _ */
}

i.e.,

pub trait Trait {}

pub fn unsizing_to_dyn_coercion<'r, 'u, ConcreteTy>(
    r: &'r mut ConcreteTy
) -> &'r mut (dyn 'u + Trait)
where
    ConcreteTy : 'u + Trait,
{
    r /* as _ */
}
  • this is also true behind &'r, or when inside a Box<>, or {A,R}c: "any" pointer indirection works. In our case, we are especially interested in the &'r mut form of pointer indirection.

  • for instance, let's pick Bounds… = Debug, and impl 'usability + Bounds… = u8:

    &'r mut u8
    // ↓ can be coërced into: ↓
    &'r mut (dyn 'static + Debug) // since u8 : 'static + Debug
    
    // As well as:
    &'r mut u8
    // ↓ can be coërced into: ↓
    &'r mut (dyn 'whatever + Debug) // since u8 : 'whatever + Debug

This is called an unsizing coërcion, insofar:

  1. we started off a Sized referee type: whichever concrete impl 'usability + Bounds… type we had to begin with, such as u8 in our examples;

  2. we end up with a !Sized referee type: dyn '_ + Debug.

So we have effectively un-Sized the referee / we have made our reference go from referring to a concrete, Sized type, to it now referring to some "abstract" / hard-to-trace dyn-erased type which impls the desired bounds.

Hence the name. In fact, in nightly Rust, there is an unstable special trait in the language to encode this property: Unsize<dyn 'usability + Bounds…>.

We have the following property:

impl 'usability + Bounds: Unsize<dyn 'usability + Bounds>

i.e., in pseudo-cody-math parlance:

if<T : 'usability + Bounds…> {
    T : Unsize<dyn 'usability + Bounds>,
    // i.e.
    PtrTo<T> can be coërced into PtrTo<dyn 'usability + Bounds>
    // e.g., picking `PtrTo['r]<T> = &'r mut T`:
    `&'r mut T` can be coerced into `&'r mut (dyn 'usability + Bounds)`
}

Back to our curious case of dyn JaminButton + 'lt, it turns out that there is a very similar/related type-coërcion rule that Rust can apply. That which I have personally decided to dub:

The "re-unsizing coërcion"

Why such a name? Well, for starters, w.r.t. the previous chapter, consider:

pub trait Trait {}

pub fn unsizing_to_dyn_coercion<'r, 'u>(
    //                not `'u`!
    //                  👇
    r: &'r mut (impl 'static + Trait)
) -> &'r mut (dyn 'u + Trait)
{
    r /* as _ */
}

Indeed, this is:

pub trait Trait {}

pub fn unsizing_to_dyn_coercion<'r, 'u, ConcreteTy>(
    r: &'r mut ConcreteTy
) -> &'r mut (dyn 'u + Trait)
where
    //            not `'u`!
    //              👇
    ConcreteTy : 'static + Trait,
{
    r /* as _ */
}

And if ConcreteTy : 'static + Trait then we have, no matter the choice of 'u: ConcreteTy : 'u + Trait, since 'static : 'u, so we have:

impl 'static + Trait : 'u + Trait
// thus,
impl 'static + Trait : Unsize<dyn 'u + Trait>

// i.e.,

&'r mut (impl 'static + Bounds)
// ↓ can be coërced into: ↓
&'r mut (dyn 'u + Bounds)

Notice something? This is starting to look like "lifetime shrinkage behind &mut"! Now we only need to replace the impl in the first line with dyn, and we'll be golden!

Well, that is exactly what the "re-unsizing coërcion" is about!

The idea / proof of soundness would be as follows:

  1. we start off a &'r mut (dyn 'static + Bounds…).

  2. we don't know which, concrete, Sized, type was at the origin of this dyn, but there must exist one such type! Let's call it Mysterious.

    For &'r mut Mysterious to have been coërcible into a &'r mut (dyn 'static + Bounds…), the following will have needed to hold:

  3. Mysterious : Unsize<dyn 'static + Bounds…>
    // i.e.,
    Mysterious : 'static + Bounds// Since, `'static : 'u`, we have:
    Mysterious : 'u + Bounds// i.e.,
    Mysterious : Unsize<'dyn 'u + Bounds…>
  4. fn this_is_sound<'r, 'u>(
        r: &'r mut Mysterious,
    ) -> &'r mut (dyn 'u + Bounds)
    {
        r /* as _ */
    }
  5. This reasoning holds for any Mysterious type that might be "inhabiting" / at the origin of the dyn 'static + Bounds… erased instance, for any such instance.

  6. Since all of their "original/stemming" concrete types would be sound to coërce and unsize into a dyn 'u + Bounds (behind the &'r mut), then the compiler might just as well allow "such a coërcion" from happing directly from the dyn 'static + Bounds… middle-person.

//! for any 'r, 'u  (where `'u : 'r`),

for every `instance: &'r mut (dyn 'static + Bounds)`
exists type `Mysterious` so that `instance` stemmed from `&'r mut Mysterious`:
  - i.e.,
    `transmute::<_, &'r mut Mysterious>(instance)` would be sound;
  - and also:
    Mysterious : Unsize<dyn 'static + Bounds…>
    Mysterious : 'static + BoundsMysterious : 'u + Bounds// since 'static : 'u
    Mysterious : Unsize<dyn 'u + Bounds…>
    So a `&'r mut Mysterious -> &'r mut (dyn 'u + Bounds)` coërcion is sound too.
We apply both, and end up with `&'r mut (dyn 'u + Bounds)`.

Thus, such an API would be sound, modulo implementation.

What about the implementation? Well, this is where the "re-unsizing" part comes into play: the lifetime of + 'usability has no real/concrete existence in the codegen/compiled binary, it is a purely theoretical/abstract, type-level, static-analysis concern: all the &'r mut (dyn '_ + Bounds…) types have all the very same layout and ABI, no matter the + '_.

So the implementation is as simple as:

fn DIY_reunsizing<'r, 'u>(
    r: &'r mut (dyn 'static + Bounds)
) -> &'r mut (dyn 'u + Bounds)
{
    unsafe {
        ::core::mem::transmute(r)
    }
}

All this is something the compiler can do for us, and in fact, it very much does: the re-unsizing coërcion:

fn reunsizing_coercion<'r, 'u>(
    r: &'r mut (dyn 'static + Bounds)
) -> &'r mut (dyn 'u + Bounds)
{
    r /* as _ */
}

As you can see, it can even do it implicitly for us (no need for the explicit as _ coërcion-nudge); at the very least, when all the surrounding types are properly specified, rather than inferred.

Back to our problem at hand:

How come, if &'r mut (dyn '_ + Tr) is invariant over + '_, that the following snippet compiles fine?

trait Tr {}

struct A(Option<Box<dyn 'static + Tr>>);

impl A {
    fn f<'r>(&'r mut self) -> Option<&'r mut (dyn 'r + Tr)> {
        self.0.as_mut().map(|x: &'r mut Box<dyn 'static + Tr>| {
            &mut **x // : &'r mut (dyn 'static + Tr)
     // not a subtype of: &'r mut (dyn 'r + Tr)
        })
    }
}

We now know the answer: thanks to a re-unsizing coërcion!

And all this is what was happening, perhaps quite incredibly tersely, in the original snippet:

trait Tr {}

struct A(Option<Box<dyn Tr>>);

impl A {
    fn f(&mut self) -> Option<&mut dyn Tr> {
        self.0.as_mut().map(|x| { &mut **x })
        //                      ^^^^^^^^^^^^
        //                   re-unsizing coërcion!
    }
}

And it so happens that, for internal implementation details of the compiler, the trailing expression of a { … } braced body of a closure, is eligible to {re-,}unsizing coërcions.

Whereas a non-braced body is not necessarily such!

//! After `rustfmt`:

trait Tr {}
struct A(Option<Box<dyn Tr>>);
impl A {
    fn f(&mut self) -> Option<&mut dyn Tr> {
        self.0.as_mut().map(|x| &mut **x)
        //                     ^        ^
        //             no braces, no re-unsizing coërcion.
    }
}

Hence my idea of crafting this snippet to trick rustfmt into doing a Bad Thing™, since rustfmt does operate under the assumption that for a given <expr>, || <expr> and || { <expr> } ought to be semantically equivalent.

Well, we have just seen how this is not 100% correct, at least insofar {re-,}unsizing coërcions are concerned 🤡

  • Bonus 🤡🤡🤡🤡

    For what is worth, changing the presence of braces in || <expr> vs. || { <expr> } can also be semantically meaningful insofar the parsing can also be affected by such a change!

    For instance, {} & Foo is parsed as () & Foo in pure expression context such as || {} & Foo, but parsed as {}; &Foo when in the "statement" context inside a braced block…

    #[derive(Debug)]
    struct Foo;
    
    #[derive(Debug)]
    struct Bar;
    
    impl ::core::ops::BitAnd<Foo> for () {
        type Output = &'static Bar;
    
        fn bitand(self: (), _: Foo) -> Self::Output { &Bar }
    }
    
    fn main() {
        // Parsed as `|| ({} & Foo)` i.e. `() & Foo` == `&Bar`
        let f = || {} & Foo;
        // Parsed as `{}; &Foo` == `&Foo`
        let g = || { {} & Foo };
    
        dbg!(f()); // prints `Bar`
        dbg!(g()); // prints `Foo`
    }

    But rest assured, rustfmt appears to be aware of this one, it won't remove the braces of that g definition 🧠


Footnotes

  1. weirdly enough, this includes 'static or named lifetimes introduced from an outer scope (e.g., impl<…>-level generics). And this only works for syntactically-spelled out such lifetimes. So if, for instance, we had Self = Foo<'a>, then using Foo<'a> will count as a syntactical mention of a named lifetime ('a), whereas using Self will not; and ditto for type aliases (given type StaticStr = &'static str;, using StaticStr will not count as a lifetime-naming occurrence, whereas &'static str will) 🫠

  2. currently, rust denies if there is more than one, resulting in an actual requirement of there being exactly one + 'usability bound. But, for what is worth, this could technically be loosened down to at least one + 'usability bound, as in, more than one could, in the future, be deemed acceptable; although, let's be honest, it would be rather useless too, and thus, probably a big code smell. Indeed, it would mean the resulting thing would be usable within the union of the 'a and 'b regions, which, barring other further 'x : 'y information, represents a region which can very well span beyond 'a (imagine a Venn diagram of some A and B properties: the two "potatoes" cover an areä bigger than that of just the left potato A), so the actual type shall not be infected by 'a (lest it become unusable beyond 'a), and ditto for 'b, resulting in a dyn … + 'a + 'b type which stemmed from a type infected by neither 'a nor 'b! It might as well have been marked 'static.

  3. assuming, of course, Referee not to be itself using 'r.

  4. fn pointers only (and some cases of fn items). Most notably: this does not apply to impl/dyn Fn(…) -> _ signatures, which instead follow the (in)variance rules of impl/dyn Trait<…>.

  5. The "users" Rust forum: https://users.rust-lang.org

  6. it is actually possible to design a sound lifetime-infected Any, but the requirement when doing so is for the lifetime(s) (potentially!) at play in such a trait to be actual/direct generic lifetime parameters of the trait, rather than trying to piggyback off + 'usability. So no dyn LifetimeAny + 'lt, but rather, dyn LifetimeAny<'lt>.

@p-avital
Copy link

This post is the embodiment of this energy and I love it!
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment