Skip to content

Can references to uninhabited types ever be valid? #413

Open
rust-lang/reference
#2337

Description

@RalfJung

This discussion presupposes that "Does reference validity depend on the contents of memory in any way" (#414) is answered "no". This issue is only about references to uninhabited types; references to any other type are not relevant here.

So, in general we allow references to point to invalid data. Is the following UB?

let x: &! = transmute(&());

Since there is no recursive validity for references, the answer might be "no". However, the reason we could consider making this UB is that all of the arguments in #412 do not apply: to determine whether an &! is valid, we do not have to check memory, do any potentially racy reads, or anything like that. We can just answer the question with "no". Recursive validity is tricky in part because references can "bounce" between being valid and invalid as the contents of memory change; but for references to uninhabited types, this concern does not apply.

Basically, we can think of ! having an impossible-to-satisfy alignment requirement, and therefore &! is invalid since it is never properly aligned. (I don't really want to fold this into the alignment check, but the point is that we already have the validity of &T depend on T, namely via alignment -- I see no fundamental reason why we couldn't do the same with inhabitedness.)

Activity

  1. digama0 commented on Jun 6, 2023

    @digama0

    Does this issue also cover &Void and &(!, T)? The latter is problematic since I think there are situations where you can have partially uninit references (and this also partially overlaps with #346). But maybe it suffices to say that an initialized &T (i.e. a value returned from a function, like your transmute example) requires that T be inhabited, even though uninitialized or partially initialized places of type &! or &(!, T) can exist.

  2. RalfJung commented on Jun 6, 2023

    @RalfJung
    MemberAuthor

    Does this issue also cover &Void and &(!, T)?

    Yes.

    While you can have partially initialized locals of type (!, T), I don't think you can have references to them. Do you have an example for that?

  3. chorman0773 commented on Jun 6, 2023

    @chorman0773
    Contributor

    Is there a reason why they should be invalid unless &T is invalid if the T is?

  4. RalfJung commented on Jun 6, 2023

    @RalfJung
    MemberAuthor

    One motivation is being able to replace the entire body of an &self function where self is uninhabited with just unreachable_unchecked(). Back at the last Rust All hands (2019 I think?), someone expressed strong interest in that (code size) optimization.

  5. digama0 commented on Jun 6, 2023

    @digama0

    One motivation is being able to replace the entire body of an &self function where self is uninhabited with just unreachable_unchecked(). Back at the last Rust All hands (2019 I think?), someone expressed strong interest in that (code size) optimization.

    This has been mentioned elsewhere and I think you agreed to it, but that is not a very compelling motivation since you can just put match *self {} in the body to get the same optimization. (If I had my druthers match self {} would also work; this does not strictly speaking require a "no" answer to the OP question.)

  6. digama0 commented on Jun 6, 2023

    @digama0

    While you can have partially initialized locals of type (!, T), I don't think you can have references to them. Do you have an example for that?

    I don't, at least not without writing an unsafe block that would be UB under the OP question (more or less by definition). A variation that may or may not count is &Box<!>; I'm not sure whether this counts as inhabited or not, and it might actually thread the needle between competing notions of inhabitedness since you can empty-match on it and move out a ! (although maybe you want to call this a safety violation), even though by structural analysis of Box<!> you get a Unique which could just be a dangling pointer.

    My main motivation for considering that case specially is that the borrow checker does allow for types like &(T, U) to have more complex initialization states, and future lang proposals might reify these as types (e.g. view types), in which case you might have a function that takes an argument of type &{only T is init} (!, T) and you would not be able to assume that this type is uninhabited.

  7. Diggsey commented on Jun 6, 2023

    @Diggsey

    One motivation is being able to replace the entire body of an &self function where self is uninhabited with just unreachable_unchecked(). Back at the last Rust All hands (2019 I think?), someone expressed strong interest in that (code size) optimization.

    Wouldn't it be almost always trivial to prove that such a function is unreachable anyway? Assuming reachability checks occur post-monomorphisation.

  8. saethlin commented on Jun 6, 2023

    @saethlin
    Member

    One motivation is being able to replace the entire body of an &self function where self is uninhabited with just unreachable_unchecked(). Back at the last Rust All hands (2019 I think?), someone expressed strong interest in that (code size) optimization.

    I feel like we could do an experiment here to figure out if this is actually worth anything?

  9. digama0 commented on Jun 6, 2023

    @digama0

    If it's unreachable, then you wouldn't even need an unreachable_unchecked, the whole function could be deleted. But it could be a field in a trait object, for example, in which case you do need the function (but it could be a dangling pointer? 🤔 )

  10. RalfJung commented on Jun 7, 2023

    @RalfJung
    MemberAuthor

    This has been mentioned elsewhere and I think you agreed to it, but that is not a very compelling motivation since you can just put match *self {} in the body to get the same optimization. (If I had my druthers match self {} would also work; this does not strictly speaking require a "no" answer to the OP question.)

    I'm trying to remember the old discussion. The concern might have been that these are trait impls generated automatically / via proc macros for tons of types. So we can't just add custom code into the body.

    I think it was @cramertj who raised this wish back then, not sure if they still remember any details though.

    Wouldn't it be almost always trivial to prove that such a function is unreachable anyway? Assuming reachability checks occur post-monomorphisation.

    If it's a public function in a lib crate, how would that be trivial?

  11. saethlin commented on Jun 7, 2023

    @saethlin
    Member
  12. RalfJung commented on Jun 8, 2023

    @RalfJung
    MemberAuthor

    I guess the benchmarks don't contain auto-derived code for uninhabited types. Also as a MIR opt this is less effective than it could be, if generic types and their impls are monomorphized for an uninhabited type.

  13. RalfJung commented on Jun 8, 2023

    @RalfJung
    MemberAuthor

    My main motivation for considering that case specially is that the borrow checker does allow for types like &(T, U) to have more complex initialization states

    I have never seen this happen, so this is news to me. What is an example of that?

  14. digama0 commented on Jun 8, 2023

    @digama0

    I take it back, the borrow checker will reason about partially initialized Box<(T, U)> but not &(T, U). However I can come up with examples that would plausibly benefit from it:

    #[derive(Default)]
    struct Foo {
      a: String,
      ...
      z: String,
    }
    let mut x = Foo::default();
    drop(x.z);
    || println!("{}, ..., {}", x.a, ..., x.y);

    Currently, the returned closure captures &x.a, ..., &x.y as references and hence would be 25 pointers large, but a future optimization could capture &x instead with a borrow state indicating that x.z is uninit.

  15. 109 remaining items

  16. RalfJung commented on May 28, 2026

    @RalfJung
    MemberAuthor

    I think we have to treat this more like a layout question: the unspecified layout algorithm will determine whether a type is inhabited. It guarantees that certain types are inhabited, but for the remainder, either answer is valid. The layout algorithm can change in the future.

    We discussed this in the opsem meeting and the general vibe was to continue down this route: "inhabited" is an underspecified function on types similar to "size" and "alignment". We do not guarantee anything about that function except "if the validity invariant of a type is satisfiable, then the type is inhabited".

    @rfcbot resolve recursive-types

  17. rust-rfcbot commented on May 28, 2026

    @rust-rfcbot
    Collaborator

    🔔 This is now entering its final comment period, as per the review above. 🔔

  18. rust-rfcbot commented on Jun 7, 2026

    @rust-rfcbot
    Collaborator

    The final comment period, with a disposition to merge, as per the review above, is now complete.

    As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.

  19. added a commit that references this issue on Jul 17, 2026
  20. added a commit that references this issue on Jul 17, 2026
  21. added a commit that references this issue on Sep 23, 2026
  22. added a commit that references this issue on Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions