RFC 1414: rvalue_static_promotion

lang (typesystem | borrowck | machine | const-eval | value-promotion)

Summary

Promote constexpr rvalues to values in static memory instead of stack slots, and expose those in the language by being able to directly create 'static references to them. This would allow code like let x: &'static u32 = &42 to work.

Motivation

Right now, when dealing with constant values, you have to explicitly define const or static items to create references with 'static lifetime, which can be unnecessarily verbose if those items never get exposed in the actual API:

fn return_x_or_a_default(x: Option<&u32>) -> &u32 {
    if let Some(x) = x {
        x
    } else {
        static DEFAULT_X: u32 = 42;
        &DEFAULT_X
    }
}
fn return_binop() -> &'static Fn(u32, u32) -> u32 {
    const STATIC_TRAIT_OBJECT: &'static Fn(u32, u32) -> u32
        = &|x, y| x + y;
    STATIC_TRAIT_OBJECT
}

This workaround also has the limitation of not being able to refer to type parameters of a containing generic functions, eg you can't do this:

fn generic<T>() -> &'static Option<T> {
    const X: &'static Option<T> = &None::<T>;
    X
}

However, the compiler already special cases a small subset of rvalue const expressions to have static lifetime - namely the empty array expression:

let x: &'static [u8] = &[];

And though they don't have to be seen as such, string literals could be regarded as the same kind of special sugar:

let b: &'static [u8; 4] = b"test";
// could be seen as `= &[116, 101, 115, 116]`

let s: &'static str = "foo";
// could be seen as `= &str([102, 111, 111])`
// given `struct str([u8]);` and the ability to construct compound
// DST structs directly

With the proposed change, those special cases would instead become part of a general language feature usable for custom code.

Detailed design

Inside a function body's block:

The UnsafeCell restrictions are there to ensure that the promoted value is truly immutable behind the reference.

Examples:

// OK:
let a: &'static u32 = &32;
let b: &'static Option<UnsafeCell<u32>> = &None;
let c: &'static Fn() -> u32 = &|| 42;

let h: &'static u32 = &(32 + 64);

fn generic<T>() -> &'static Option<T> {
    &None::<T>
}

// BAD:
let f: &'static Option<UnsafeCell<u32>> = &Some(UnsafeCell { data: 32 });
let g: &'static Cell<u32> = &Cell::new(); // assuming conf fn new()

These rules above should be consistent with the existing rvalue promotions in const initializer expressions:

// If this compiles:
const X: &'static T = &<constexpr foo>;

// Then this should compile as well:
let x: &'static T = &<constexpr foo>;

Implementation

The necessary changes in the compiler did already get implemented as part of codegen optimizations (emitting references-to or memcopies-from values in static memory instead of embedding them in the code).

All that is left do do is "throw the switch" for the new lifetime semantic by removing these lines: https://github.com/rust-lang/rust/blob/29ea4eef9fa6e36f40bc1f31eb1e56bf5941ee72/src/librustc/middle/mem_categorization.rs#L801-L807

(And of course fixing any fallout/bitrot that might have happened, adding tests, etc.)

Drawbacks

One more feature with seemingly ad-hoc rules to complicate the language...

Alternatives, Extensions

It would be possible to extend support to &'static mut references, as long as there is the additional constraint that the referenced type is zero sized.

This again has precedence in the array reference constructor:

// valid code today
let y: &'static mut [u8] = &mut [];

The rules would be similar:

The zero-sized restriction is there because aliasing mutable references are only safe for zero sized types (since you never dereference the pointer for them).

Example:

fn return_fn_mut_or_default(&mut self) -> &FnMut(u32, u32) -> u32 {
    self.operator.unwrap_or(&mut |x, y| x * y)
    // ^ would be okay, since it would be translated like this:
    // const STATIC_TRAIT_OBJECT: &'static mut FnMut(u32, u32) -> u32
    //     = &mut |x, y| x * y;
    // self.operator.unwrap_or(STATIC_TRAIT_OBJECT)
}

let d: &'static mut () = &mut ();
let e: &'static mut Fn() -> u32 = &mut || 42;

There are two ways this could be taken further with zero-sized types:

  1. Remove the UnsafeCell restriction if the type of the rvalue is zero-sized.
  2. The above, but also remove the constexpr restriction, applying to any zero-sized rvalue instead.

Both cases would work because one can't cause memory unsafety with a reference to a zero sized value, and they would allow more safe code to compile.

However, they might complicated reasoning about the rules more, especially with the last one also being possibly confusing in regards to side-effects.

Not doing this means:

Unresolved questions

None, beyond "Should we do alternative 1 instead?".