-
Notifications
You must be signed in to change notification settings - Fork 17
Towards dynamic const-qualify #27
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
Changes from 9 commits
a18aafc
a9c7047
50a1829
cbd7e7e
53d0200
7f8c81c
87c3c87
95acdf2
a29f147
1d2b7b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,25 +1,32 @@ | ||
# Const promotion | ||
|
||
"Promotion" 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; EXPR }`, but only if `EXPR` | ||
qualifies. | ||
["(Implicit) Promotion"][promotion-rfc] 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; EXPR}`, but only if `EXPR` qualifies. | ||
|
||
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. | ||
|
||
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]. | ||
That's why we have to be very conservative with what can and cannot be promoted. | ||
|
||
[promotion-rfc]: https://github.com/rust-lang/rfcs/blob/master/text/1414-rvalue_static_promotion.md | ||
[warn-rfc]: https://github.com/rust-lang/rfcs/blob/master/text/1229-compile-time-asserts.md | ||
|
||
## Rules | ||
|
||
### 1. Panics | ||
|
||
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 (the code would have compiled just fine if we hadn't | ||
promoted), but we must be sure to error correctly at run-time. In the MIR, this | ||
looks roughly like | ||
breaking compilation, but we must be sure to error correctly at run-time. In | ||
the MIR, this looks roughly like | ||
|
||
``` | ||
_tmp1 = CheckedSub (const 0usize) (const 1usize) | ||
|
@@ -46,6 +53,9 @@ 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. | ||
|
||
*Dynamic check.* The Miri engine already dynamically detects panics, but the | ||
main point of promoteds is ruling them out statically. | ||
|
||
### 2. Const safety | ||
|
||
We have explained what happens when evaluating a promoted panics, but what about | ||
|
@@ -62,7 +72,7 @@ everything, so the only possible remaining failure are panics. | |
|
||
However, things get more tricky when `const` and `const fn` are involved. | ||
|
||
For `const`, based on the const safety check described [here](const_safety.md), | ||
For `const`, based on the const safety check described [here](const_safety.md#const-safety-check-on-values), | ||
we can rely on there not being const-unsafe values in the `const`, so we should | ||
be able to promote freely. For example: | ||
|
||
|
@@ -89,18 +99,18 @@ but to abort compilation of a program that would have compiled fine if we would | |
not have decided to promote. It is the responsibility of `foo` to not fail this | ||
way when working with const-safe arguments. | ||
|
||
### 3. Constraints on constants | ||
For this reason, only `const fn` that were explicitly marked with the | ||
`#[rustc_promotable]` attribute are subject to promotion. | ||
|
||
All the [extra restrictions for constants](const.md) beyond const safety also | ||
apply to promoteds, for the same reason: Evaluating the expression at | ||
compile-time instead of run-time should not alter program behavior. | ||
*Dynamic check.* The Miri engine already dynamically detects const safety | ||
violations, but the main point of promoteds is ruling them out statically. | ||
|
||
### 4. Drop | ||
### 3. Drop | ||
|
||
Expressions containing "needs drop" types | ||
can never be promoted. If such an expression were promoted, the `Drop` impl would | ||
never get called on the value, even though the user did not explicitly request such | ||
behavior by using an explicit `const` or `static` item. | ||
Expressions returning "needs drop" types can never be promoted. If such an | ||
expression were promoted, the `Drop` impl would never get called on the value, | ||
even though the user did not explicitly request such behavior by using an | ||
explicit `const` or `static` item. | ||
|
||
As expression promotion is essentially the silent insertion of a `static` item, and | ||
`static` items never have their `Drop` impl called, the `Drop` impl of the promoted | ||
|
@@ -111,6 +121,41 @@ it is unlikely to be the desired behavior in most cases and very likey to be con | |
to the user. If such behavior is desired, the user can still use an explicit `static` | ||
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. | ||
|
||
## `&` in `const` and `static` | ||
|
||
Promotion is also responsible for making code like this work: | ||
|
||
```rust | ||
const FOO: &'static i32 = { | ||
let x = &13; | ||
x | ||
}; | ||
``` | ||
|
||
However, since this is in explicit const context, we are less strict about | ||
promotion in this situation: all function calls are promoted, not just | ||
`#[rustc_promotable]` functions. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @RalfJung I think my confusion really occurs because of this paragraph. It seems to apply that both There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah yes. That's because of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That example works if you make |
||
|
||
Promotion is *not* involved in something like this: | ||
|
||
```rust | ||
const EMPTY_BYTES: &Vec<u8> = &Vec::new(); | ||
|
||
const NESTED: &'static Vec<u8> = { | ||
// This does not work when we have an inner scope: | ||
let x = &Vec::new(); //~ ERROR: temporary value dropped while borrowed | ||
x | ||
}; | ||
``` | ||
|
||
In `EMPTY_BYTES`, the reference obtains the lifetime of the "enclosing scope", | ||
similar to how `let x = &mut x;` creates a reference whose lifetime lasts for | ||
the enclosing scope. This is decided during MIR building already, and does not | ||
involve promotion. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This "enclosing scope" rule doesn't make sense to me. I don't know how it should extend to arbitrary cases. Is it the braces that cause the problem or the assignment to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't know. I am paraphrasing @eddyb here.
Good point, will do. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm I guess I should edit this in the light of our unresolved terminology. Is the entire "Promotion is not involved" incorrect, or is there something there that makes sense? I was trying to explain why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See my latest comment for what is actually confusing me here. The "enclosing scope" rule makes sense intuitively, but the details are still not clear to me. That's my problem though, not this PR's. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have done some edits. @ecstatic-morse does this resolve your confusion? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The enclosing scope rule is the same one that makes these two different: {
let r = &foo(); // compiles
bar(r);
} {
let r = id(&foo()); // doesn't compile
bar(r);
} Except instead of I don't remember the exact name of these semantics, maybe "rvalue/temporary lifetime/scope"? @nikomatsakis would be more helpful here, sorry. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I call these "temporary lifetimes" -- I've been meaning to writeup some text about them for the rust reference. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I call these "temporary lifetimes" -- I've been meaning to writeup some text about them for the rust reference. |
||
|
||
## Open questions | ||
|
||
* There is a fourth kind of CTFE failure -- resource exhaustion. What do we do | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Statics | ||
|
||
Statics (`static`, `static mut`) are the simplest kind of compile-time evaluated data: | ||
* The user explicitly requested them to be evaluated at compile-time, | ||
so evaluation errors from computing the initial value of a static are no concern | ||
(in other words, [const safety](const_safety.md) is mostly not an issue). | ||
* They observably get evaluated *once*, with the result being put at some address known at run-time, | ||
so there are no fundamental restrictions on what statics can do. | ||
* The compiler checks that statics are `Sync`, justifying sharing their address across threads. | ||
* [Constants](const.md) and [promoteds](promotion.md) are not allowed to read from statics, | ||
so their final value does not have have to be [const-valid](const_safety.md#const-safety-check-on-values) in any meaningful way. | ||
As of 2019-08, we do check them for validity anyway, to be conservative; and indeed constants could be allowed to read from frozen statics. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this would be nice personally; less times to invent an ephemeral constant just to please the check and you also get to use exported statics from other crates. Any future compat hazards with some new feature? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Discovered a problem... If we want to have a scheme like rust-lang/rfcs#2492 but centered around There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Centril sorry, I do not follow... why is it harder for separate compilation to support There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case it would be a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As long as whatever instantiates the |
||
|
||
## `Drop` | ||
|
||
The compiler rejects intermediate values (created and discarded during the computation of a static initializer) that implement `Drop`. | ||
The reason for this is simply that the `Drop` implementation might be non-`const fn`. | ||
This restriction can be lifted once `const impl Drop for Type` (or something similar) is supported. | ||
|
||
```rust | ||
struct Foo; | ||
|
||
impl Drop for Foo { | ||
fn drop(&mut self) { | ||
println!("foo dropped"); | ||
} | ||
} | ||
|
||
static FOOO: Foo = Foo; // Ok, drop is never run | ||
|
||
// Not ok, cannot run `Foo::drop` because it's not a const fn | ||
static BAR: i32 = (Foo, 42).1; | ||
``` | ||
|
||
*Dynamic check.* The Miri engine dynamically checks that this is done correctly | ||
by not permitting calls of non-`const` functions. |
Uh oh!
There was an error while loading. Please reload this page.