diff --git a/promotion.md b/promotion.md index 8281888..7df5b6d 100644 --- a/promotion.md +++ b/promotion.md @@ -2,80 +2,74 @@ "Promotion" is the act of splicing a part of a MIR computation out into a separate self-contained MIR body which is evaluated at compile-time like a -constant. +constant. This mechanism has been introduced by [RFC 1414][promotion-rfc] with +the goal of equipping some references-to-temporaries with a `'static` lifetime, +which is sometimes called "lifetime extension". -## Promotion contexts - -There are a few different contexts where promotion is beneficial. - -### Lifetime extension +[promotion-rfc]: https://github.com/rust-lang/rfcs/blob/master/text/1414-rvalue_static_promotion.md -"Lifetime extension" is a mechanism that affects code like `&3`: -Instead of putting it on the stack, the `3` is allocated in global static memory -and a reference with lifetime `'static` is provided. This is essentially an -automatic transformation turning `&EXPR` into `{ const _PROMOTED = &EXPR; -_PROMOTED }`, but only if `EXPR` qualifies. Topmost projections are not -promoted, so `&EXPR.proj1.proj2` turns into `{ const _PROMOTED = &EXPR; -&(*_PROMOTED).proj1.proj2 }`. +Promotion / lifetime extension affects code like `&3`: Instead of putting it on +the stack, the `3` is allocated in global static memory and a reference with +lifetime `'static` is provided. This is essentially an automatic transformation +turning `&EXPR` into `{ const _PROMOTED = &EXPR; _PROMOTED }`, but only if +`EXPR` qualifies. Topmost projections are not promoted, so `&EXPR.proj1.proj2` +turns into `{ const _PROMOTED = &EXPR; &(*_PROMOTED).proj1.proj2 }`. Note that promotion happens on the MIR, not on surface-level syntax. This is relevant when discussing e.g. handling of panics caused by overflowing arithmetic. -Lifetime extension is described in [RFC 1414][promotion-rfc]. The RFC uses the -word "promotion" to refer exclusively to lifetime extension, since this was the -first context where promotion was done. - -[promotion-rfc]: https://github.com/rust-lang/rfcs/blob/master/text/1414-rvalue_static_promotion.md - -### Non-`Copy` array initialization - -Another promotion context, the initializer of an array expression, was -introduced in [RFC 2203][]. Here, promotion allows arrays of -non-`Copy` types to be initialized idiomatically, for example -`[Option::>::None; 32]`. - -[RFC 2203]: https://github.com/rust-lang/rfcs/blob/master/text/2203-const-repeat-expr.md - -### `#[rustc_args_required_const(...)]` and inline assembly `const` operands - -Additionally, some platform intrinsics require certain parameters to be -immediates (known at compile-time). We use the `#[rustc_args_required_const]` -attribute, introduced in -[rust-lang/rust#48018](https://github.com/rust-lang/rust/pull/48018), to -specify these parameters and (aggressively, see below) try to promote the -corresponding arguments. - -Similarly, inline assembly has `const` operands, which are treated the same way -as `rustc_args_required_const` arguments. +## Promotion and fallability of const-evaluation -## Implicit and explicit promotion +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*. Thus, if CTFE fails but the code would have worked fine at +run-time, we broke the user's code for no good reason. 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]. -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*. -Thus, if CTFE fails but the code would have worked fine at run-time, we broke the user's code for no good reason. -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]. -We call this *implicit* promotion, and we have to be very conservative with what can and cannot be implicitly promoted. - -CTFE of implicitly promoted code must never fail to evaluate except if the -run-time code also would have failed. This means we cannot permit calling -arbitrary `const fn`, as discussed in detail in +For example: +```rust +fn foo() { + if false { + let x = &(1/0); + } +} +``` +If we performed promotion here, this would turn into +```rust +fn foo() { + if false { + const _PROMOTED = &(1/0); + let x = _PROMOTED; + } +} +``` +When compiling this function, we have to evaluate all constants that occur +inside the function body, even if they might only be used in dead code. This +means we have to evaluate `_PROMOTED`, which will error -- and now what, should +we halt compilation? That would be wrong since there is no problem with this +code, the "bad" division never actually happens as it occurs in dead code. +(Note that the considerations would be the same even if `foo` were a `const +fn`.) + +As a consequence, we only promote code that can never fail to evaluate (see +[RFC 3027]). This ensures that even if promotion happens inside dead code, this +will not turn a "runtime error in dead code" (which is not an error at all) into +a compile-time error. In particular, we cannot promote calls to arbitrary `const +fn`, as discussed in detail in [rust-lang/const-eval#19](https://github.com/rust-lang/const-eval/issues/19). -Thus, only functions marked `#[rustc_promotable]` are implicitly promotable (see -below). - -On the other hand, when a user passes an expression to a function with -`#[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 can thus be less conservative. This is called *explicit* -promotion. +Thus, only functions marked `#[rustc_promotable]` are promotable. -Currently, the following are considered explicit promotion contexts: -* `#[rustc_args_required_const]` arguments and inline assembly `const` operands everywhere. -* 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. -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.) +[RFC 3027]: https://rust-lang.github.io/rfcs/3027-infallible-promotion.html -In these contexts, we promote calls to arbitrary `const fn`. +There is one exception to this rule: the bodies of `const`/`static` +initializers. This code is never compiled, so we do not actually have to +evaluate constants that occur in dead code. If we are careful enough during +compilation, we can ensure that only constants whose value is *actually needed* +are evaluated. We thus can be more relaxed about promotion; in practice, what +this means is that we will promote calls to arbitrary `const fn`, not just those +marked `#[rustc_promotable]`. [See below][static access] for another special case in promotion analysis: 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. ## "enclosing scope" rule -Notice that some code involving `&` *looks* like it relies on lifetime -extension but actually does not: +Notice that some code involving `&` *looks* like it relies on promotion / +lifetime extension but actually does not: ```rust const EMPTY_BYTES: &Vec = &Vec::new(); // Ok without lifetime extension @@ -117,20 +111,20 @@ 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. -Note that there is no point in doing additional dynamic checks here. The entire point of -the promotion restrictions is to avoid failing compilation for code that would -have been fine without promotion. The best a dynamic check could do is tell us -after the fact that we should not have promoted something, but then it is -already too late -- and the dynamic checks for that are exactly the ones we are -already doing for constants and statics. +Note that there is no point in doing additional dynamic checks to ensure that we +do get these restrictions right. The entire point of the promotion restrictions +is to avoid failing compilation for code that would have been fine without +promotion. The best a dynamic check could do is tell us after the fact that we +should not have promoted something, but then it is already too late -- and the +dynamic checks for that are exactly the ones we are already doing for constants +and statics. -### Panics +### Panics, overflow and bounds checks -Promotion is not allowed to throw away side effects. This includes panicking. -Let us look at what happens when we promote `&(0_usize - 1)` in a debug build: -We have to avoid erroring at compile-time, because that would be promotion -breaking compilation, but we must be sure to error correctly at run-time. In -the MIR, this looks roughly like +Let us look at what happens when we promote `&(0_usize - 1)` in a debug build. +This code is promoted even though we cannot promote code that could fail, and +this code will fail with an overflow error! What is happening? We have to look +at the underlying MIR representation of this code to explain what happens: ``` _tmp1 = CheckedSub (const 0usize) (const 1usize) @@ -142,26 +136,27 @@ _res = &_tmp2 ``` Both `_tmp1` and `_tmp2` are promoted. `_tmp1` evaluates to `(~0, true)`, so -the assertion will always fail at run-time. Computing `_tmp2` fails with a -panic, which is thrown away -- so we have no result. In principle, we could -generate any code for this because we know the code is unreachable (the -assertion is going to fail). Just to be safe, we generate a call to -`llvm.trap`. - -As long as CTFE only panics when run-time code would also have panicked, this -works out correctly: The MIR already contains provisions for what to do on -panics (unwind edges etc.), so when CTFE panics we can generate code that -hard-codes a panic to happen at run-time. In other words, *promotion relies on -CTFE correctly implementing both normal program behavior and panics*. An -earlier version of miri used to panic on arithmetic overflow even in release -mode. This breaks promotion, because now promoting code that would work (and -could not panic!) at run-time leads to a compile-time CTFE error. +the assertion will always fail at run-time. Computing `_tmp2` evaluates to `~0`. + +In other words, the actually failing check is not promoted, only the computation +that serves as input to the check is promoted. + +An earlier version of Miri used to error on arithmetic overflow even in release +mode. This breaks promotion, because now promoting code like `_tmp1` would +introduce promotes that fail to evaluate, which is not acceptable as explained +above! + +Something similar but more subtle happens when promoting array accesses: the +bounds check is not promoted, but the array access is. However, before accepting +a temporary for promotion, we ensure that array accesses are definitely +in-bounds. This leads to MIR without bounds checks, but we know the array access +will always succeed. ### Const safety -We have explained what happens when evaluating a promoted panics, but what about -other kinds of failure -- what about hitting an unsupported operation or -undefined behavior? To make sure this does not happen, only const safe code +We have explained how we ensure that evaluating a promoted does not panic, but +what about other kinds of failure -- what about hitting an unsupported operation +or undefined behavior? To make sure this does not happen, only const safe code gets promoted. The exact details for `const safety` are discussed in [here](const_safety.md).