-
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 3 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 | ||
|
@@ -89,18 +99,15 @@ 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 | ||
|
||
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 +118,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 could be less strict about | ||
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. We are less strict. You can promote any const fn call inside a constant 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, good to know! Are there any other differences? We could in principle be even more aggressive. 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. iirc we are running just the regular const checks on promoteds inside constants and const fns. So there's no third system, it's exactly as if you wrote an explicit constant 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. Isnt promotion also about identifying which parts of the MIR to promote, and then actually patch the MIR to replace uses of the data by a promoted? Somehow this still needs to do something extra inside constants I think, or will it just trigger at every 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. So implementation wise the difference is 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. So I asked @eddyb about this terminology but must have misunderstood what he said. 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'm confused. So 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 guess I am confused by the "implicit" part. The promotion to a separate static is also implicit inside a const body. If the thing inside a normal 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. yea... it's not the best naming scheme... Maybe we should call the thing in non-const fns "restricted promotion" and the other cases just "promotion" or "unrestricted 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. That doesn't convey why it is restricted, though. "implicit" did that nicely, but was not precise enough. "promotion of run-time code" is verbose but informative. "run-time promotion" is somewhat misleading... "run-time-code promotion"? |
||
promotion in this situation. | ||
|
||
Promotion is *not* involved in something like this: | ||
|
||
```rust | ||
#![feature(const_vec_new)] | ||
oli-obk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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,35 @@ | ||
# 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. | ||
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) in any meaningful way | ||
(but as of 2019-08, we do check them for validity anyway, to be conservative). | ||
|
||
## `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.