Skip to content

promotion tweaks #31

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

Merged
merged 9 commits into from
Nov 27, 2019
Merged
Show file tree
Hide file tree
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
5 changes: 5 additions & 0 deletions const.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ note the FIXME added [by this PR](https://github.com/rust-lang/rust/pull/63955):
for untyped data in a constant, we currently just *make* it immutable, instead
of checking properly.

Note that a constant *referring to* some already existing mutable memory is
fine: inlining that reference everywhere has the same behavior as computing a
new reference each time. In both cases, there exists exactly one instance of
the mutable memory that everything references.

### 3. `Sync`

Finally, the same constant reference is actually shared across threads. This is
Expand Down
130 changes: 83 additions & 47 deletions promotion.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Const promotion

"Promotion" is the act of guaranteeing that code not written in a const context
(e.g. initalizer of a `const` or `static`, or an array length expression) will
be run at compile-time.
"Promotion" is the act of guaranteeing that code *not* written in an (explicit)
const context will be run at compile-time. Explicit const contexts include the
initializer of a `const` or `static`, or an array length expression.

## Promotion contexts

Expand Down Expand Up @@ -57,11 +57,14 @@ actually put on the stack. In this way, lifetime extension is an "implicit
promotion context": the user did not ask for the value to be promoted.

On the other hand, when a user passes an expression to a function with
`#[rustc_args_required_const]`, they are explicitly asking for that expression
`#[rustc_args_required_const]`, the only way for this code to compile is to promote it.
In that sense, the user is explicitly asking for that expression
to be evaluated at compile-time even though they have not written it in a
`const` declaration. We call this an "explicit promotion context".

Currently, non-`Copy` array initialization is treated as an implicit context.
Currently, non-`Copy` array initialization is treated as an implicit context,
because the code could compile even without promotion (namely, if the result
type is `Copy`).

The distinction between these two determines whether calls to arbitrary `const
fn`s (those without `#[rustc_promotable]`) are promotable (see below). See
Expand Down Expand Up @@ -112,48 +115,10 @@ expressions are actually eligible for promotion? We refer to eligible
expressions as "promotable" and describe the restrictions on such expressions
below.

### Named locals

Promotable expressions cannot refer to named locals. This is not a technical
limitation with the CTFE engine. While writing `let x = {expr}` outside of a
const context, the user likely expects that `x` will live on the stack and be
initialized at run-time. Although this is not (to my knowledge) guaranteed by
the language, we do not wish to violate the user's expectations here.

### Single assignment

We only promote temporaries that are assigned to exactly once. For example, the
lifetime of the temporary whose reference is assigned to `x` below will not be
extended.

```rust
let x: &'static i32 = &if cfg!(windows) { 0 } else { 1 };
```

Once again, this is not a fundamental limitation in the CTFE engine; we are
perfectly capable of evaluating such expressions at compile time. However,
determining the promotability of complex expressions would require more
resources for little benefit.

### Access to a `const` or `static`

When accessing a `const` in a promotable context, the restrictions on single
assignment and named locals do not apply to the body of the `const`. All other
restrictions, notably that the result of the `const` cannot be `Drop` or mutable
through a reference still apply. For instance, while the previous example was
not legal, the following would be:

```rust
const BOOL: i32 = {
let ret = if cfg!(windows) { 0 } else { 1 };
ret
};

let x: &'static i32 = &BOOL;
```

An access to a `static` is only promotable within the initializer of
another `static`.
First of all, expressions have to be [allowed in constants](const.md). The
restrictions described there are needed because we want `const` to behave the
same as copying the `const` initializer everywhere the constant is used; we need
the same property when promoting expressions. But we need more.

### Panics

Expand Down Expand Up @@ -259,6 +224,77 @@ or `const` item and refer to that.
*Dynamic check.* The Miri engine could dynamically check this by ensuring that
the result of computing a promoted is a value that does not need dropping.

### Access to a `const` or `static`

When accessing a `const` in a promotable context, its value gets computed
at compile-time anyway, so we do not have to check the initializer. However, the
restrictions described above still apply for the *result* of the promoted
computation: in particular, it must be a valid `const` (i.e., it cannot
introduce interior mutability) and it must not require dropping.
Copy link
Contributor

@ecstatic-morse ecstatic-morse Nov 25, 2019

Choose a reason for hiding this comment

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

It's worth noting that these restrictions apply only in the context lifetime extension. We can initialize an array, which also uses the implicit rules, with a constant containing interior mutability (i.e. [CELL; 4]). My revisions failed to do this.

Copy link
Member Author

Choose a reason for hiding this comment

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

But [&CELL; 4] gets rejected, I hope?

Copy link
Contributor

@ecstatic-morse ecstatic-morse Nov 25, 2019

Choose a reason for hiding this comment

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

That compiles on stable because &T is Copy for all T. Any use of it will cause a borrow check error since the access to CELL has a temporary lifetime. Promotion fails for &CELL even if it's in an array initializer if that's what you're asking.

Copy link
Member Author

Choose a reason for hiding this comment

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

Promotion fails for &CELL even if it's in an array initializer if that's what you're asking.

Good.

But then behavior seems to be the same for arrays and lifetime extension? It's just that lifetime extension always involves a borrow.


For instance the following would be legal even though calls to `do_it` are not
eligible for implicit promotion:

```rust
const fn do_it(x: i32) -> i32 { 2*x }
const ANSWER: i32 = {
let ret = do_it(21);
ret
};

let x: &'static i32 = &ANSWER;
```

An access to a `static` is only promotable within the initializer of another
`static`. This is for the same reason that `const` initializers
[cannot access statics](const.md#reading-statics).

Crucially, however, the following is *not* legal:

```rust
const X: Cell<i32> = Cell::new(5); // ok
const XREF: &Cell<i32> = &X; // not ok
fn main() {
let x: &'static _ = &X; // not ok
}
```

Just like allowing `XREF` would be a problem because, by the inlining semantics,
every user of `XREF` should get their own `Cell`; it would also be a problem to
promote here because if that code gets executed multiple times (e.g. inside a
loop), it should get a new `Cell` each time.

### Named locals

Promotable expressions cannot refer to named locals. This is not a technical
limitation with the CTFE engine. While writing `let x = {expr}` outside of a
const context, the user likely expects that `x` will live on the stack and be
initialized at run-time. Although this is not (to my knowledge) guaranteed by
the language, we do not wish to violate the user's expectations here.

Note that constant-folding still applies: the optimizer may compute `x` at
compile-time and even inline it everywhere if it can show that this does not
observably alter program behavior. Promotion is very different from
constant-folding as promotion can introduce observable differences in behavior
(if const-evaluation fails) and as it is *guaranteed* to happen in some cases
(and thus exploited by the borrow checker). This is reflected in the fact that
promotion affects lifetimes, but constant folding does not.

### Single assignment

We only promote temporaries that are assigned to exactly once. For example, the
lifetime of the temporary whose reference is assigned to `x` below will not be
extended.

```rust
let x: &'static i32 = &if cfg!(windows) { 0 } else { 1 };
```

Once again, this is not a fundamental limitation in the CTFE engine; we are
perfectly capable of evaluating such expressions at compile time. However,
determining the promotability of complex expressions would require more
resources for little benefit.

## Open questions

* There is a fourth kind of CTFE failure -- resource exhaustion. What do we do
Expand Down