|
| 1 | +- Feature Name: infallible_lifetime_extension |
| 2 | +- Start Date: 2020-11-08 |
| 3 | +- RFC PR: [rust-lang/rfcs#3027](https://github.com/rust-lang/rfcs/pull/3027) |
| 4 | +- Rust Issue: [rust-lang/rust#80619](https://github.com/rust-lang/rust/issues/80619) |
| 5 | + |
| 6 | +# Summary |
| 7 | +[summary]: #summary |
| 8 | + |
| 9 | +Restrict (implicit) [promotion][rfc1414], such as lifetime extension of rvalues, to infallible operations. |
| 10 | + |
| 11 | +[rfc1414]: https://github.com/rust-lang/rfcs/blob/master/text/1414-rvalue_static_promotion.md |
| 12 | + |
| 13 | +# Motivation |
| 14 | +[motivation]: #motivation |
| 15 | + |
| 16 | +## Background on promotion and lifetime extension |
| 17 | + |
| 18 | +Rvalue promotion (as it was originally called) describes the process of taking an rvalue that can be computed at compile-time, and "promoting" it to a constant, so that references to that rvalue can have `'static` lifetime. |
| 19 | +It has been introduced by [RFC 1414][rfc1414]. |
| 20 | +The scope of what exactly is being promoted in which context has been extended over the years in an ad-hoc manner, and the underlying mechanism of promotion (to extract a part of a larger body of code into a separate constant) is now also used for purposes other than making references have `'statc` lifetime. |
| 21 | +To account for this, the const-eval WG [agreed on the following terminology][promotion-status]: |
| 22 | +* Making references have `'static` lifetime is called "lifetime extension". |
| 23 | +* The underlying mechanism of extracting part of some code into a constant is called "promotion". |
| 24 | + |
| 25 | +Promotion is currently used for four compiler features: |
| 26 | +* lifetime extension |
| 27 | +* non-`Copy` array repeat expressions |
| 28 | +* functions where some arguments must be known at compile-time (`#[rustc_args_required_const]`) |
| 29 | +* `const` operands of `asm!` |
| 30 | + |
| 31 | +These uses of promotion fall into two categories: |
| 32 | +* *Explicit* promotion refers to promotion where not promoting is simply not an option: `#[rustc_args_required_const]` and `asm!` *require* the value of this expression to be known at compile-time. |
| 33 | +* *Implicit* promotion refers to promotion that might not be required: a reference might not actually need to have `'static` lifetime, and an array repeat expression could be `Copy` (or the repeat count no larger than 1). |
| 34 | + |
| 35 | +For more details, see the [const-eval WG writeup][promotion-status]. |
| 36 | + |
| 37 | +## The problem with implicit promotion |
| 38 | + |
| 39 | +Explicit promotion is mostly fine as-is. |
| 40 | +This RFC is concerned with implicit promotion. |
| 41 | +The problem with implicit promotion is best demonstrated by the following example: |
| 42 | + |
| 43 | +```rust |
| 44 | +fn make_something() { |
| 45 | + if false { &(1/0) } |
| 46 | +} |
| 47 | +``` |
| 48 | + |
| 49 | +If the compiler decides to do implicit promotion here, the code is changed to something like |
| 50 | + |
| 51 | +```rust |
| 52 | +fn make_something() { |
| 53 | + if false { |
| 54 | + const VAL: &i32 = &(1/0); |
| 55 | + VAL |
| 56 | + } |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +However, this code would fail to compile! |
| 61 | +When doing code generation for a function, all its constants have to be evaluated, including the ones in dead code, since in general we cannot know that we are compiling dead code. |
| 62 | +(In fact, there is even code that [relies on failing constants stopping compilation](https://github.com/rust-lang/rust/issues/67191).) |
| 63 | +When evaluating `VAL`, a panic is triggered due to division by zero, so any code that needs to know the value of `VAL` is stuck as there is no such value. |
| 64 | + |
| 65 | +This is a problem because the original code (pre-promotion) works just fine: the division never actually happens. |
| 66 | +It is only because the compiler decided to extract the division into a separately evaluated constant that it even becomes a problem. |
| 67 | +Notice that this is a problem only for implicit promotion, because with explicit promotion, the value *has* to be known at compile-time -- so stopping compilation if the value cannot be determined is the right behavior. |
| 68 | + |
| 69 | +To solve this problem, every part of the compiler that works with constants needs to be able to handle the case where the constant *has no defined value*, and continue in some correct way. |
| 70 | +This is hard to get right, and has lead to a number of problems over the years: |
| 71 | +* There has been at least one [soundness issue](https://github.com/rust-lang/rust/issues/50814). |
| 72 | +* There are still outstanding [diagnostic issues](https://github.com/rust-lang/rust/issues/61821). |
| 73 | +* Promotion needs a special [exception in const-value validation](https://github.com/rust-lang/rust/issues/67534). |
| 74 | +* All code handling constants has to carry [extra complexity to support promotion](https://github.com/rust-lang/rust/issues/75461) |
| 75 | + |
| 76 | +This RFC proposes to fix all these problems at once, by restricting implicit promotion to those expression whose evaluation cannot fail. |
| 77 | +This is the last step in a series of changes that have been going on for quite some time, starting with the [introduction](https://github.com/rust-lang/rust/pull/53851) of the `#[rustc_promotable]` attribute to control which function calls may be subject to implicit promotion (the original RFC said that all calls to `const fn` should be promoted, but as user-defined `const fn` got closer and closer, that seemed less and less like a good idea, due to all the ways in which evaluating a `const fn` can fail). |
| 78 | +Together with [some planned changes for evaluation of regular constants](https://github.com/rust-lang/rust/issues/71800), this means that all CTFE failures can be made hard errors, greatly simplifying the parts of the compiler that trigger evaluation of constants and handle the resulting value or error. |
| 79 | + |
| 80 | +For more details, see [the MCP that preceded this RFC](https://github.com/rust-lang/lang-team/issues/58). |
| 81 | + |
| 82 | +[promotion-status]: https://github.com/rust-lang/const-eval/blob/33053bb2c9a0c6a17acd3116dd47bbb360e060db/promotion.md |
| 83 | + |
| 84 | +# Guide-level explanation |
| 85 | +[guide-level-explanation]: #guide-level-explanation |
| 86 | + |
| 87 | +(Based on [RFC 1414][rfc1414]) |
| 88 | + |
| 89 | +Inside a function body's block: |
| 90 | + |
| 91 | +- If a shared reference to a constexpr rvalue is taken. (`&<constexpr>`), |
| 92 | +- And the constexpr does not contain a `UnsafeCell { ... }` constructor, |
| 93 | +- And the constexpr only consists of operations that will definitely succeed to |
| 94 | + evaluate at compile-time, |
| 95 | +- And the resulting value does not need dropping, |
| 96 | +- Then instead of translating the value into a stack slot, translate |
| 97 | + it into a static memory location and give the resulting reference a |
| 98 | + `'static` lifetime. |
| 99 | + |
| 100 | +Operations that definitely succeed at the time of writing the RFC include: |
| 101 | +- literals of any kind |
| 102 | +- constructors (struct/enum/union/tuple) |
| 103 | +- struct/tuple field accesses |
| 104 | +- arithmetic and logical operators that do not involve division: `+`/`-`/`*`, all bitwise and shift operators, all unary operators |
| 105 | + |
| 106 | +Note that arithmetic overflow is not a problem: an addition in debug mode is compiled to a `CheckedAdd` MIR operation that never fails, which returns an `(<int>, bool)`, and is followed by a check of said `bool` to possibly raise a panic. |
| 107 | +We only ever promote the `CheckedAdd`, so evaluation of the promoted will never fail, even if the operation overflows. |
| 108 | +For example, `&(1 + u32::MAX)` turns into something like: |
| 109 | +```rust |
| 110 | +const C: (u32, bool) = CheckedAdd(1, u32::MAX); // evaluates to (0, true). |
| 111 | +assert!(C.1 == false); |
| 112 | +&C.0 |
| 113 | +``` |
| 114 | +See [this prior RFC](https://github.com/rust-lang/rfcs/blob/master/text/1211-mir.md#overflow-checking) for further details. |
| 115 | + |
| 116 | +However, also note that operators being infallible is more subtle than it might seem. |
| 117 | +In particular, it requires that all constants of integer type (and even all integer-typed fields of all constants) be proper integers, not pointers cast to integers. |
| 118 | +The following code shows a problematic example: |
| 119 | +```rust |
| 120 | +const FOO: usize = &42 as *const i32 as usize; |
| 121 | +let x: &usize = &(FOO * 3); |
| 122 | +``` |
| 123 | +`FOO*3` cannot be evaluated during CTFE, so to ensure that multiplication is infallible, we need to ensure that all constants used in promotion are proper integers. |
| 124 | +This is currently ensured by the "validity check" that is performed on the final value of each constant: the check recursively traverses the type of the constant and ensures that the data matches that type. |
| 125 | + |
| 126 | +Operations that might fail include: |
| 127 | +- `/`/`%` |
| 128 | +- `panic!` (including the assertion that follows `Checked*` arithmetic to ensure that no overflow happened) |
| 129 | +- array/slice indexing |
| 130 | +- any unsafe operation |
| 131 | +- `const fn` calls (as they might do any of the above) |
| 132 | + |
| 133 | +Notably absent from *both* of the above list is dereferencing a reference. |
| 134 | +This operation is, in principle, infallible---but due to the concern mentioned above about validity of consts, it is only infallible if the validity check in constants traverses through references. |
| 135 | +Currently, the check stops when hitting a reference to a static, so currently, dereferencing a reference can *not* be considered an infallible operation for the purpose of promotion. |
| 136 | + |
| 137 | +# Reference-level explanation |
| 138 | +[reference-level-explanation]: #reference-level-explanation |
| 139 | + |
| 140 | +See above for (hopefully) all the required details. |
| 141 | +What exactly the rules will end up being for which operations can be promoted will depend on experimentation to avoid breaking too much existing code, as discussed below. |
| 142 | + |
| 143 | +# Drawbacks |
| 144 | +[drawbacks]: #drawbacks |
| 145 | + |
| 146 | +The biggest drawback is that this will break some existing code. |
| 147 | +Compared to the status quo, this means the following expressions are not implicitly promoted any more: |
| 148 | +* Division, modulo, array/slice indexing |
| 149 | +* `const fn` calls in `const`/`static` bodies (`const fn` are already not being implicitly promoted in `fn` and `const fn` bodies) |
| 150 | + |
| 151 | +If code relies on implicit promotion of these operations, it will stop to compile. |
| 152 | +Crater runs should be used all along the way to ensure that the fall-out is acceptable. |
| 153 | +The language team will be involved (via FCP) in each breaking change to make this judgment call. |
| 154 | +If too much code is broken, various ways to weaken this proposal (at the expense of more technical debt, sometimes across several parts of the compiler) are [described blow][rationale-and-alternatives]. |
| 155 | + |
| 156 | +The long-term plan is that such code can switch to [inline `const` expressions](2920-inline-const.md) instead. |
| 157 | +However, inline `const` expressions are still in the process of being implemented, and for now are specified to not support code that depends on generic parameters in the context, which is a loss of expressivity when compared with implicit promotion. |
| 158 | +More complex work-around are possible for this using associated `const`, but they can become quite tedious. |
| 159 | + |
| 160 | +# Rationale and alternatives |
| 161 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 162 | + |
| 163 | +The rationale has been described with the motivation. |
| 164 | + |
| 165 | +Unless we want to keep supporting fallible const-evaluation indefinitely, the main alternatives are devising more precise analyses to determine if some operation is infallible. |
| 166 | +For example, we could still perform implicit promotion for division and modulo if the divisor is a non-zero literal. |
| 167 | +We could also have `CheckedDiv` and `CheckedMod` operations that, similar to operations like `CheckedAdd`, always returns a result of the right type together with a `bool` saying if the result is valid. |
| 168 | +We could still perform *array* indexing if the index is a constant and in-bounds. |
| 169 | +For slices, we could have an analysis that predicts the (minimum) length of the slice. |
| 170 | +Notice that promotion happens in generic code and can depend on associated constants, so we cannot, in general, *evaluate* the implicit promotion candidate to check if that causes any errors. |
| 171 | + |
| 172 | +We could also decide to still perform implicit promotion of potentially fallible operations in the bodies of `const`s and `static`s. |
| 173 | +(This would mean that the RFC only changes behavior of implicit promotion in `fn` and `const fn` bodies.) |
| 174 | +This is possible because that code is not subject to code generation, it is only interpreted by the CTFE engine. |
| 175 | +The engine will only evaluate the part of the code that is actually being run, and thus can avoid evaluating promoteds in dead code. |
| 176 | +However, this means that all other consumers of this code (such as pretty-printing and optimizations) must *not* evaluate promoteds that they encounter, since that evaluation may fail. |
| 177 | +This will incur technical debt in all of those places, as we need to carefully ensure not to eagerly evaluate all constants that we encounter. |
| 178 | +We also need to be careful to still evaluate all user-defined constants even inside promoteds in dead code (because, remember, code may rely on the fact that compilation will fail if any constant that is syntactically used in a function fails to evaluated). |
| 179 | +Note that this is *not* an option for code generation, i.e., for code in `fn` and `const fn`: all code needs to be translated to LLVM, even possibly dead code, so we have to evaluate all constants that we encounter. |
| 180 | + |
| 181 | +If there are some standard library `const fn` that cannot fail to evaluate, and that form the bulk of the function calls being implicitly promoted, we could add the `#[rustc_promotable]` attribute to them to enable implicit promotion. |
| 182 | +This will not help, however, if there is plenty of code relying on implicit promotion of user-defined `const fn`. |
| 183 | + |
| 184 | +Conversely, if this plan all works out, one alternative proposal that goes even further is to restrict implicit promotion to expressions that would be permitted in a pattern. |
| 185 | +This would avoid adding a new class of expression in between "patterns" and "const-evaluable". |
| 186 | +On the other hand, it is much more restrictive (basically allowing only literals and constructors), and does not actually help simplify the compiler. |
| 187 | + |
| 188 | +# Prior art |
| 189 | +[prior-art]: #prior-art |
| 190 | + |
| 191 | +A few changes have landed in the recent past that already move us, step-by-step, towards the goal outlined in this RFC: |
| 192 | +* Treat `const fn` like `fn` for promotability: https://github.com/rust-lang/rust/pull/75502, https://github.com/rust-lang/rust/pull/76411 |
| 193 | +* Do not promote `union` field accesses: https://github.com/rust-lang/rust/pull/77526 |
| 194 | + |
| 195 | +# Unresolved questions |
| 196 | +[unresolved-questions]: #unresolved-questions |
| 197 | + |
| 198 | +The main open question is to what extend existing code relies on lifetime extension of fallible operations, i.e., if we can get away with the plan outlined here. |
| 199 | +(Lifetime extension is currently the only stable form of implicit promotion, and thus the only one relevant for backwards compatibility.) |
| 200 | +In `fn` and `const fn`, only a few fallible operations remain: division, modulo, and slice/array indexing. |
| 201 | +In `const` and `static`, we additionally promote calls to arbitrary `const fn`, which of course could fail in arbitrary ways -- crater experiments will have to show if code actually relies on this. |
| 202 | +A fall-back plan in case this RFC would break too much code has been [described above][rationale-and-alternatives]. |
| 203 | + |
| 204 | +# Future possibilities |
| 205 | +[future-possibilities]: #future-possibilities |
| 206 | + |
| 207 | +A potential next step after this RFC could be to tackle the remaining main promotion "hack", the `#[rustc_promotable]` attribute. |
| 208 | +We now know exactly what this attribute expresses: this `const fn` may never fail to evaluate (in particular, it may not panic). |
| 209 | +This provides a theoretical path to stabilization of this attribute, backed by an analysis that ensures that the function indeed does not panic. |
| 210 | +(However, once inline `const` expressions with generic parameters are stable, this does not actually grant any extra expressivity, just a slight increase in convenience.) |
0 commit comments