Skip to content

Commit 139f190

Browse files
authored
Merge pull request #67 from RalfJung/promotion
update promotion docs
2 parents 07328a7 + c95bbf4 commit 139f190

File tree

1 file changed

+88
-93
lines changed

1 file changed

+88
-93
lines changed

Diff for: promotion.md

+88-93
Original file line numberDiff line numberDiff line change
@@ -2,80 +2,74 @@
22

33
"Promotion" is the act of splicing a part of a MIR computation out into a
44
separate self-contained MIR body which is evaluated at compile-time like a
5-
constant.
5+
constant. This mechanism has been introduced by [RFC 1414][promotion-rfc] with
6+
the goal of equipping some references-to-temporaries with a `'static` lifetime,
7+
which is sometimes called "lifetime extension".
68

7-
## Promotion contexts
8-
9-
There are a few different contexts where promotion is beneficial.
10-
11-
### Lifetime extension
9+
[promotion-rfc]: https://github.com/rust-lang/rfcs/blob/master/text/1414-rvalue_static_promotion.md
1210

13-
"Lifetime extension" is a mechanism that affects code like `&3`:
14-
Instead of putting it on the stack, the `3` is allocated in global static memory
15-
and a reference with lifetime `'static` is provided. This is essentially an
16-
automatic transformation turning `&EXPR` into `{ const _PROMOTED = &EXPR;
17-
_PROMOTED }`, but only if `EXPR` qualifies. Topmost projections are not
18-
promoted, so `&EXPR.proj1.proj2` turns into `{ const _PROMOTED = &EXPR;
19-
&(*_PROMOTED).proj1.proj2 }`.
11+
Promotion / lifetime extension affects code like `&3`: Instead of putting it on
12+
the stack, the `3` is allocated in global static memory and a reference with
13+
lifetime `'static` is provided. This is essentially an automatic transformation
14+
turning `&EXPR` into `{ const _PROMOTED = &EXPR; _PROMOTED }`, but only if
15+
`EXPR` qualifies. Topmost projections are not promoted, so `&EXPR.proj1.proj2`
16+
turns into `{ const _PROMOTED = &EXPR; &(*_PROMOTED).proj1.proj2 }`.
2017

2118
Note that promotion happens on the MIR, not on surface-level syntax. This is
2219
relevant when discussing e.g. handling of panics caused by overflowing
2320
arithmetic.
2421

25-
Lifetime extension is described in [RFC 1414][promotion-rfc]. The RFC uses the
26-
word "promotion" to refer exclusively to lifetime extension, since this was the
27-
first context where promotion was done.
28-
29-
[promotion-rfc]: https://github.com/rust-lang/rfcs/blob/master/text/1414-rvalue_static_promotion.md
30-
31-
### Non-`Copy` array initialization
32-
33-
Another promotion context, the initializer of an array expression, was
34-
introduced in [RFC 2203][]. Here, promotion allows arrays of
35-
non-`Copy` types to be initialized idiomatically, for example
36-
`[Option::<Box<i32>>::None; 32]`.
37-
38-
[RFC 2203]: https://github.com/rust-lang/rfcs/blob/master/text/2203-const-repeat-expr.md
39-
40-
### `#[rustc_args_required_const(...)]` and inline assembly `const` operands
41-
42-
Additionally, some platform intrinsics require certain parameters to be
43-
immediates (known at compile-time). We use the `#[rustc_args_required_const]`
44-
attribute, introduced in
45-
[rust-lang/rust#48018](https://github.com/rust-lang/rust/pull/48018), to
46-
specify these parameters and (aggressively, see below) try to promote the
47-
corresponding arguments.
48-
49-
Similarly, inline assembly has `const` operands, which are treated the same way
50-
as `rustc_args_required_const` arguments.
22+
## Promotion and fallability of const-evaluation
5123

52-
## Implicit and explicit promotion
24+
On top of what applies to [consts](const.md), promoteds suffer from the
25+
additional issue that *the user did not ask for them to be evaluated at
26+
compile-time*. Thus, if CTFE fails but the code would have worked fine at
27+
run-time, we broke the user's code for no good reason. Even if we are sure we
28+
found an error in the user's code, we are only allowed to
29+
[emit a warning, not a hard error][warn-rfc].
5330

54-
On top of what applies to [consts](const.md), promoteds suffer from the additional issue that *the user did not ask for them to be evaluated at compile-time*.
55-
Thus, if CTFE fails but the code would have worked fine at run-time, we broke the user's code for no good reason.
56-
Even if we are sure we found an error in the user's code, we are only allowed to [emit a warning, not a hard error][warn-rfc].
57-
We call this *implicit* promotion, and we have to be very conservative with what can and cannot be implicitly promoted.
58-
59-
CTFE of implicitly promoted code must never fail to evaluate except if the
60-
run-time code also would have failed. This means we cannot permit calling
61-
arbitrary `const fn`, as discussed in detail in
31+
For example:
32+
```rust
33+
fn foo() {
34+
if false {
35+
let x = &(1/0);
36+
}
37+
}
38+
```
39+
If we performed promotion here, this would turn into
40+
```rust
41+
fn foo() {
42+
if false {
43+
const _PROMOTED = &(1/0);
44+
let x = _PROMOTED;
45+
}
46+
}
47+
```
48+
When compiling this function, we have to evaluate all constants that occur
49+
inside the function body, even if they might only be used in dead code. This
50+
means we have to evaluate `_PROMOTED`, which will error -- and now what, should
51+
we halt compilation? That would be wrong since there is no problem with this
52+
code, the "bad" division never actually happens as it occurs in dead code.
53+
(Note that the considerations would be the same even if `foo` were a `const
54+
fn`.)
55+
56+
As a consequence, we only promote code that can never fail to evaluate (see
57+
[RFC 3027]). This ensures that even if promotion happens inside dead code, this
58+
will not turn a "runtime error in dead code" (which is not an error at all) into
59+
a compile-time error. In particular, we cannot promote calls to arbitrary `const
60+
fn`, as discussed in detail in
6261
[rust-lang/const-eval#19](https://github.com/rust-lang/const-eval/issues/19).
63-
Thus, only functions marked `#[rustc_promotable]` are implicitly promotable (see
64-
below).
65-
66-
On the other hand, when a user passes an expression to a function with
67-
`#[rustc_args_required_const]`, the only way for this code to compile is to
68-
promote it. In that sense, the user is explicitly asking for that expression to
69-
be evaluated at compile-time even though they have not written it in a `const`
70-
declaration. We can thus be less conservative. This is called *explicit*
71-
promotion.
62+
Thus, only functions marked `#[rustc_promotable]` are promotable.
7263

73-
Currently, the following are considered explicit promotion contexts:
74-
* `#[rustc_args_required_const]` arguments and inline assembly `const` operands everywhere.
75-
* Everything inside the bodies of `const` and `static` items. (Note: this is handled separately from "explicit contexts" in promotion analysis, but the effect is the same.
76-
The arguments given above for justifying explicit promotion do not apply here. Currently, this works out because failing to evaluate one of these promoteds just leads to a warning, but longer-term it would be desirable to turn evaluation failures into hard errors, which for these promoteds means we have to guarantee that we only evaluate them on-demand.)
64+
[RFC 3027]: https://rust-lang.github.io/rfcs/3027-infallible-promotion.html
7765

78-
In these contexts, we promote calls to arbitrary `const fn`.
66+
There is one exception to this rule: the bodies of `const`/`static`
67+
initializers. This code is never compiled, so we do not actually have to
68+
evaluate constants that occur in dead code. If we are careful enough during
69+
compilation, we can ensure that only constants whose value is *actually needed*
70+
are evaluated. We thus can be more relaxed about promotion; in practice, what
71+
this means is that we will promote calls to arbitrary `const fn`, not just those
72+
marked `#[rustc_promotable]`.
7973

8074
[See below][static access] for another special case in promotion analysis:
8175
accesses and references to statics are only promoted inside other statics.
@@ -84,8 +78,8 @@ accesses and references to statics are only promoted inside other statics.
8478

8579
## "enclosing scope" rule
8680

87-
Notice that some code involving `&` *looks* like it relies on lifetime
88-
extension but actually does not:
81+
Notice that some code involving `&` *looks* like it relies on promotion /
82+
lifetime extension but actually does not:
8983

9084
```rust
9185
const EMPTY_BYTES: &Vec<u8> = &Vec::new(); // Ok without lifetime extension
@@ -117,20 +111,20 @@ restrictions described there are needed because we want `const` to behave the
117111
same as copying the `const` initializer everywhere the constant is used; we need
118112
the same property when promoting expressions. But we need more.
119113

120-
Note that there is no point in doing additional dynamic checks here. The entire point of
121-
the promotion restrictions is to avoid failing compilation for code that would
122-
have been fine without promotion. The best a dynamic check could do is tell us
123-
after the fact that we should not have promoted something, but then it is
124-
already too late -- and the dynamic checks for that are exactly the ones we are
125-
already doing for constants and statics.
114+
Note that there is no point in doing additional dynamic checks to ensure that we
115+
do get these restrictions right. The entire point of the promotion restrictions
116+
is to avoid failing compilation for code that would have been fine without
117+
promotion. The best a dynamic check could do is tell us after the fact that we
118+
should not have promoted something, but then it is already too late -- and the
119+
dynamic checks for that are exactly the ones we are already doing for constants
120+
and statics.
126121

127-
### Panics
122+
### Panics, overflow and bounds checks
128123

129-
Promotion is not allowed to throw away side effects. This includes panicking.
130-
Let us look at what happens when we promote `&(0_usize - 1)` in a debug build:
131-
We have to avoid erroring at compile-time, because that would be promotion
132-
breaking compilation, but we must be sure to error correctly at run-time. In
133-
the MIR, this looks roughly like
124+
Let us look at what happens when we promote `&(0_usize - 1)` in a debug build.
125+
This code is promoted even though we cannot promote code that could fail, and
126+
this code will fail with an overflow error! What is happening? We have to look
127+
at the underlying MIR representation of this code to explain what happens:
134128

135129
```
136130
_tmp1 = CheckedSub (const 0usize) (const 1usize)
@@ -142,26 +136,27 @@ _res = &_tmp2
142136
```
143137

144138
Both `_tmp1` and `_tmp2` are promoted. `_tmp1` evaluates to `(~0, true)`, so
145-
the assertion will always fail at run-time. Computing `_tmp2` fails with a
146-
panic, which is thrown away -- so we have no result. In principle, we could
147-
generate any code for this because we know the code is unreachable (the
148-
assertion is going to fail). Just to be safe, we generate a call to
149-
`llvm.trap`.
150-
151-
As long as CTFE only panics when run-time code would also have panicked, this
152-
works out correctly: The MIR already contains provisions for what to do on
153-
panics (unwind edges etc.), so when CTFE panics we can generate code that
154-
hard-codes a panic to happen at run-time. In other words, *promotion relies on
155-
CTFE correctly implementing both normal program behavior and panics*. An
156-
earlier version of miri used to panic on arithmetic overflow even in release
157-
mode. This breaks promotion, because now promoting code that would work (and
158-
could not panic!) at run-time leads to a compile-time CTFE error.
139+
the assertion will always fail at run-time. Computing `_tmp2` evaluates to `~0`.
140+
141+
In other words, the actually failing check is not promoted, only the computation
142+
that serves as input to the check is promoted.
143+
144+
An earlier version of Miri used to error on arithmetic overflow even in release
145+
mode. This breaks promotion, because now promoting code like `_tmp1` would
146+
introduce promotes that fail to evaluate, which is not acceptable as explained
147+
above!
148+
149+
Something similar but more subtle happens when promoting array accesses: the
150+
bounds check is not promoted, but the array access is. However, before accepting
151+
a temporary for promotion, we ensure that array accesses are definitely
152+
in-bounds. This leads to MIR without bounds checks, but we know the array access
153+
will always succeed.
159154

160155
### Const safety
161156

162-
We have explained what happens when evaluating a promoted panics, but what about
163-
other kinds of failure -- what about hitting an unsupported operation or
164-
undefined behavior? To make sure this does not happen, only const safe code
157+
We have explained how we ensure that evaluating a promoted does not panic, but
158+
what about other kinds of failure -- what about hitting an unsupported operation
159+
or undefined behavior? To make sure this does not happen, only const safe code
165160
gets promoted. The exact details for `const safety` are discussed in
166161
[here](const_safety.md).
167162

0 commit comments

Comments
 (0)