## The trouble with lifetime extension in consts
### Creating multiple allocations
The first problem with lifetime extension in consts is that it seriously complicates the code. Consider a constant like:
```rust=
const C: (&Vec<i32>, i32) = (&Vec::new(), 14);
```
When this constant has finished evaluating, we need to add *two* allocations to the global constant allocation store in `tcx`: one to hold the tuple, and one to hold the `Vec` that the first field of the tuple points to.
The process of moving const allocations from the interpreter that evaluated `C` to the global store is called "interning".
Due to lifetime extension, interning has to do a full recursive traversal of the result.
More subtly, this also causes problems wrt identifying allocations: if we replace the `const` above by `static`, then the two allocations created by that static should be globally unique across all crates.
This requires coming up with a stable identifier, a `DefId`, for each of these "nested" allocations.
For `const`s we [still don't do this properly](https://github.com/rust-lang/rust/issues/128775), leading to various odd issues and complicating other parts of the compiler such as GVN (which cannot rely on pointers behaving properly).
### Creating mutable allocations
Creating more allocations is mostly an implementation nuisance, but once you consider the interaction with mutability, it becomes a very subtle opsem issue and a footgun for unsafe code authors.
We *mostly* prevent this by rejecting `&mut` and `&raw mut` if the pointee may outlive the const (i.e., if it has been lifetime-extended). That check makes the non-trivial assumption that if there is a `StorageDead` for a local, then there is a `StorageDead` on every path to `Return`.
We allow `&mut` and `&raw mut` in `static mut`, relying on the `DefId` scheme to give these nested mutable allocation a stable global name.
Things, however, get more tricky around interior mutability.
We allow `&` and `&raw const` when there is no interior mutability.
Lifetime extension does not kick in for `&raw const`, and references to non-interior-mutable data are immutable, therefore this kind of code can at least only introduce new *immutable* allocations.
That was the plan, anyway. But there's a catch:
when checking whether `&` is allowed, we check whether the *value* has interior mutability.
But when opsem decides whether a shared reference is immutable, this is sometimes done based on the *type*.
This becomes a problem for code like `&None::<TypeWithUnsafeCell>`, which is allowed by the const checks but the reference is considered to be mutable by [most opsem proposals](https://github.com/rust-lang/unsafe-code-guidelines/issues/236).
This is for backwards compatibility: too much existing code does something like `&None` and expects to be able to put that in a `const`, even if the `Some` variant has an `UnsafeCell`.
We put the burden of dealing with this on our users by saying that all memory created in a `const` is immutable, even if the same code would create mutable memory if it was inlined into runtime code.
### Side note: promotion
Independently of lifetime extension, promotion [can *also* create](https://github.com/rust-lang/unsafe-code-guidelines/issues/493) constants of type `&None::<TypeWithUnsafeCell>`. This is even worse than doing it with lifetime extension since now there's not even a syntactic marker in the code indicating this!
We also put this burden on our users by saying that all memory in promoteds is immutable, but I think that's a cheap cop-out and we shouldn't get away with this. It is too subtle. We should instead steer people towards a more explicit way of indicating that they want a `&None::<TypeWithUnsafeCell>` that points to immutable memory.
### Way forward?
I think the following is not too controversial:
1. We should allow people to create `&None::<TypeWithUnsafeCell>` in consts, that point to immutable memory. We had this ability since Rust 1.0, and it is used in practice (crater at one point [showed 4k regressions](https://github.com/rust-lang/rust/pull/122789) when I tried to remove this), so there is clearly a need.
2. We should not do this *entirely implicitly*.
(2) means we should stop doing this as part of promotion. But what should people do instead? As of today, the only other thing they could do is to use lifetime extension. Is the fact that this happens in a `const` item/block, or a `static` without `mut`, clear enough to justify the sudden loss of mutability?
This can become quite subtle. For instance:
```rust=
struct SendPtr<T>(*mut T);
unsafe impl<T> Send for SendPtr<T> {}
unsafe impl<T> Sync for SendPtr<T> {}
static S: (Mutex<i32>, SendPtr<Option<Mutex<i32>>>) =
(Mutex::new(0),
SendPtr { 0: &None::<Mutex<i32>> as *const _ as *mut _ });
```
The static itself is mutable due to the `Mutex` field, but writing through that raw pointer is UB despite the fact that the shared reference from which the pointer is derived points to a `!Freeze` type!
With `super let`, one can even construct examples that do not involve any shared references:
```rust=
#![feature(super_let)]
pub const C: *mut i32 = {
super let mut x = 1;
&raw const x as *mut i32
};
```
Can we somehow restrict this so that (1) remains possible but only in ways where it is qutie obvious what happens?
My (RalfJ) personal gut feeling is that `const { &<expr> }` is fine, but if the `&` is below other expressions or when `super let` is involved then we probably don't want to accept any `!Freeze` types with `&`, and we don't want to accept `&raw const` at all.