Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Rvalue-static-promotion RFC #1414

Merged
merged 4 commits into from
Jan 6, 2017
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 198 additions & 0 deletions text/0000-rvalue_static_promotion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
- Feature Name: rvalue_static_promotion
- Start Date: 2015-12-18
- RFC PR:
- Rust Issue:

# Summary
[summary]: #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
[motivation]: #motivation

Right now, when dealing with constant values, you have to explicitly define
`const` or `static` items to create references with `'static` lifetime,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would I believe actually be a bit more powerful than the current workaround with consts and statics since those can't be parameterized over type parameters of the e.g. function they're defined in.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, indeed, you can do stuff like &None::<T> with this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, &None::<T> could be returned even if T were a type parameter, or &[T; 0] (but the array case is actually hardcoded from before any attempts at rvalue promotions).

which can be unnecessarily verbose if those items never get exposed
in the actual API:

```rust
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:

```rust
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:

```rust
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:

```rust
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
[design]: #detailed-design

Inside a function body's block:

- If a shared reference to a constexpr rvalue is taken. (`&<constexpr>`)
- And the constexpr does not contain a `UnsafeCell { ... }` constructor.
- And the constexpr does not contain a const fn call returning a type containing a `UnsafeCell`.
- Then instead of translating the value into a stack slot, translate
it into a static memory location and give the resulting reference a
`'static` lifetime.

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

Examples:

```rust
// 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:

```rust
// 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
[drawbacks]: #drawbacks

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

# Alternatives, Extensions
[alternatives]: #alternatives

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:

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

The rules would be similar:

- If a mutable reference to a constexpr rvalue is taken. (`&mut <constexpr>`)
- And the constexpr does not contain a `UnsafeCell { ... }` constructor.
- And the constexpr does not contain a const fn call returning a type containing a `UnsafeCell`.
- _And the type of the rvalue is zero-sized._
- Then instead of translating the value into a stack slot, translate
it into a static memory location and give the resulting reference a
`'static` lifetime.

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:

```rust
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The restriction about not having a destructor should probably stay even for zero-sized types.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently types with destructors are not zero-sized but that will change and I agree - there is no reason not to be conservative, for now, IMHO.

For the specific case of destructors, remember that the following are more or less equal:

let x = &EmptyDestructible;
...
let temp = EmptyDestructible;
{
    let x = &temp;
    ...
}
Drop::drop(&mut temp)

Thus, rvalue drops can be considered "mutable accesses".
And even if the destructor doesn't have any bytes to actually mutate, it has to run.
So you would be changing behavior if you remove the destructor call, and otherwise you kind of make a mess out of borrow semantics by allowing the resulting &'static EmptyDestructible to escape the scope its destructor ran in.

In the end, no UB can actually occur, but it still seems troubling to do this dance around destructors.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to use value-destructor-freedom rather than type-destructor-freedom here.


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:

- Relying on `static` and `const` items to create `'static` references, which won't work in generics.
- Empty-array expressions would remain special cased.
- It would also not be possible to safely create `&'static mut` references to zero-sized
types, though that part could also be achieved by allowing mutable references to
zero-sized types in constants.

# Unresolved questions
[unresolved]: #unresolved-questions

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