Skip to content

Commit 4c15a11

Browse files
authored
Merge pull request #27 from RalfJung/const-qualif
Towards dynamic const-qualify
2 parents 89b55a3 + 1d2b7b1 commit 4c15a11

File tree

4 files changed

+147
-42
lines changed

4 files changed

+147
-42
lines changed

README.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ which sort of is a virtual machine using `MIR` as "bytecode".
2121
## Table of Contents
2222

2323
* [Const Safety](const_safety.md)
24-
* [Constants](const.md)
25-
* [Promotion](promotion.md)
24+
* The three "kinds" of compile-time evaluated data:
25+
* [Statics](static.md) (`static`, `static mut`)
26+
* [Constants](const.md) (`const`, array sizes, non-`Copy` array initializers)
27+
* [Promoteds](promotion.md) (rvalue promotion)
2628

2729
## Related RFCs
2830

const.md

+30-22
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
# Further restrictions for constants
1+
# Constants
22

3-
The [const safety](const_safety.md) concerns apply to all computations happening
4-
at compile-time, `static` and `const` alike. However, there are some additional
5-
considerations about `const` specifically. These arise from the idea that
3+
"Constants" in this document refers to `const` bodies, array sizes, and non-`Copy` array initializers.
4+
On top of what applies to [statics](static.md), they are subject to an additional constraint: In code like
65
```rust
76
const CONST: T = EXPR;
87
```
98
is supposed to behave as-if `EXPR` was written at every use site of `CONST`.
9+
To make this work, we need to ensure [const safety](const_safety.md).
10+
11+
Based on this requirement, we allow other constants and [promoteds](promotion.md) to read from constants.
12+
This is why the value of a `const` is subject to validity checks.
1013

1114
## References
1215

@@ -23,31 +26,42 @@ const REF: &u32 = { const _VAL = EXPR; static _STATIC = EXPR; &_STATIC };
2326
(`EXPR` is assigned to a `const` first to make it subject to the restrictions
2427
discussed in this document.)
2528

26-
There are three reasons why this could be an issue.
29+
There are various reasons why this could be an issue.
2730

28-
### Pointer equality
31+
### 1. Pointer equality
2932

3033
We effectively "deduplicate" all the allocations that would otherwise locally be
3134
created at each use site of `REF`. This is observable when the program compares
3235
these pointers for equality. We consider this okay, i.e., programs may not rely
3336
on such constants all getting distinct addresses. They may not rely on them all
3437
getting the same address either.
3538

36-
### Interior mutability
39+
### 2. Interior mutability
3740

3841
If the reference has type `&Cell<i32>` it is quite clear that the program can
3942
easily observe whether two references point to the same memory even without
4043
comparing their address: Changes through one reference will affect reads through
4144
the other. So, we cannot allow constant references to types that have interior
42-
mutability (types that are not `Freeze`).
45+
mutability (types that are not `Freeze`):
46+
47+
```rust
48+
const BAD: &Cell<i32> = &Cell::new(42);
49+
// Inlining `BAD` everywhere clearly is not the same as them all pointing to the same thing.
50+
```
4351

4452
However, we can do better than that: Even if a *type* is not `Freeze`, it can
4553
have *values* that do not exhibit any interior mutability. For example, `&None`
4654
at type `&Option<Cell<i32>>` would be rejected by the naive analysis above, but
4755
is actually accepted by the compiler because we know that there is no
4856
`UnsafeCell` here that would permit interior mutability.
4957

50-
### `Sync`
58+
*Dynamic check.* The Miri engine enforces this dynamically by ensuring that the
59+
new data that is interned for a constant is all marked as immutable. However,
60+
note the FIXME added [by this PR](https://github.com/rust-lang/rust/pull/63955):
61+
for untyped data in a constant, we currently just *make* it immutable, instead
62+
of checking properly.
63+
64+
### 3. `Sync`
5165

5266
Finally, the same constant reference is actually shared across threads. This is
5367
very similar to multiple threads having a shared reference to the same `static`,
@@ -59,12 +73,11 @@ ecosystem that would break if we just started enforcing this now. See
5973
[this issue](https://github.com/rust-lang/rust/issues/49206) and the
6074
[PR attempting to fix this](https://github.com/rust-lang/rust/pull/54424/).
6175

62-
### `Drop`
76+
*Dynamic check.* It is unclear how the Miri engine could dynamically check this.
6377

64-
Values of "needs drop" types
65-
can only be used as the final initialization value of a `const` or `static` item.
66-
They may not be used as intermediate values that would be dropped before the item
67-
were initialized. As an example:
78+
### 4. Drop
79+
80+
`Drop` is actually not an issue, at least not more so than for statics:
6881

6982
```rust
7083
struct Foo;
@@ -76,19 +89,11 @@ impl Drop for Foo {
7689
}
7790

7891
const FOO: Foo = Foo; // Ok, drop is run at each use site in runtime code
79-
static FOOO: Foo = Foo; // Ok, drop is never run
8092

8193
// Not ok, cannot run `Foo::drop` because it's not a const fn
8294
const BAR: i32 = (Foo, 42).1;
8395
```
8496

85-
This restriction might be lifted in the future after trait impls
86-
may be declared `const` (https://github.com/rust-rfcs/const-eval/pull/8).
87-
88-
Note that in promoteds this restriction can never be lifted, because
89-
otherwise we would silently stop calling the `Drop` impl at runtime and
90-
pull it to much earlier (compile-time).
91-
9297
## Reading statics
9398

9499
Beyond values of reference type, we have to be careful that *computing* a
@@ -101,3 +106,6 @@ This is distinct to the concern about interior mutability above: That concern
101106
was about first computing a `&Cell<i32>` and then using it at run-time (and
102107
observing the fact that it has been "deduplicated"), this here is about using
103108
such a value at compile-time even though it might be changed at run-time.
109+
110+
*Dynamic check.* The Miri engine could check this dynamically by refusing to
111+
access mutable global memory when computing a const.

promotion.md

+77-18
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,32 @@
11
# Const promotion
22

3-
"Promotion" is a mechanism that affects code like `&3`: Instead of putting it on
4-
the stack, the `3` is allocated in global static memory and a reference with
5-
lifetime `'static` is provided. This is essentially an automatic transformation
6-
turning `&EXPR` into `{ const _PROMOTED = &EXPR; EXPR }`, but only if `EXPR`
7-
qualifies.
3+
["(Implicit) Promotion"][promotion-rfc] is a mechanism that affects code like `&3`:
4+
Instead of putting it on the stack, the `3` is allocated in global static memory
5+
and a reference with lifetime `'static` is provided. This is essentially an
6+
automatic transformation turning `&EXPR` into
7+
`{ const _PROMOTED = &EXPR; EXPR}`, but only if `EXPR` qualifies.
88

99
Note that promotion happens on the MIR, not on surface-level syntax. This is
1010
relevant when discussing e.g. handling of panics caused by overflowing
1111
arithmetic.
1212

13+
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*.
14+
Thus, if CTFE fails but the code would have worked fine at run-time, we broke the user's code for no good reason.
15+
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].
16+
That's why we have to be very conservative with what can and cannot be promoted.
17+
18+
[promotion-rfc]: https://github.com/rust-lang/rfcs/blob/master/text/1414-rvalue_static_promotion.md
19+
[warn-rfc]: https://github.com/rust-lang/rfcs/blob/master/text/1229-compile-time-asserts.md
20+
1321
## Rules
1422

1523
### 1. Panics
1624

1725
Promotion is not allowed to throw away side effects. This includes panicking.
1826
Let us look at what happens when we promote `&(0_usize - 1)` in a debug build:
1927
We have to avoid erroring at compile-time, because that would be promotion
20-
breaking compilation (the code would have compiled just fine if we hadn't
21-
promoted), but we must be sure to error correctly at run-time. In the MIR, this
22-
looks roughly like
28+
breaking compilation, but we must be sure to error correctly at run-time. In
29+
the MIR, this looks roughly like
2330

2431
```
2532
_tmp1 = CheckedSub (const 0usize) (const 1usize)
@@ -46,6 +53,9 @@ earlier version of miri used to panic on arithmetic overflow even in release
4653
mode. This breaks promotion, because now promoting code that would work (and
4754
could not panic!) at run-time leads to a compile-time CTFE error.
4855

56+
*Dynamic check.* The Miri engine already dynamically detects panics, but the
57+
main point of promoteds is ruling them out statically.
58+
4959
### 2. Const safety
5060

5161
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.
6272

6373
However, things get more tricky when `const` and `const fn` are involved.
6474

65-
For `const`, based on the const safety check described [here](const_safety.md),
75+
For `const`, based on the const safety check described [here](const_safety.md#const-safety-check-on-values),
6676
we can rely on there not being const-unsafe values in the `const`, so we should
6777
be able to promote freely. For example:
6878

@@ -89,18 +99,18 @@ but to abort compilation of a program that would have compiled fine if we would
8999
not have decided to promote. It is the responsibility of `foo` to not fail this
90100
way when working with const-safe arguments.
91101

92-
### 3. Constraints on constants
102+
For this reason, only `const fn` that were explicitly marked with the
103+
`#[rustc_promotable]` attribute are subject to promotion.
93104

94-
All the [extra restrictions for constants](const.md) beyond const safety also
95-
apply to promoteds, for the same reason: Evaluating the expression at
96-
compile-time instead of run-time should not alter program behavior.
105+
*Dynamic check.* The Miri engine already dynamically detects const safety
106+
violations, but the main point of promoteds is ruling them out statically.
97107

98-
### 4. Drop
108+
### 3. Drop
99109

100-
Expressions containing "needs drop" types
101-
can never be promoted. If such an expression were promoted, the `Drop` impl would
102-
never get called on the value, even though the user did not explicitly request such
103-
behavior by using an explicit `const` or `static` item.
110+
Expressions returning "needs drop" types can never be promoted. If such an
111+
expression were promoted, the `Drop` impl would never get called on the value,
112+
even though the user did not explicitly request such behavior by using an
113+
explicit `const` or `static` item.
104114

105115
As expression promotion is essentially the silent insertion of a `static` item, and
106116
`static` items never have their `Drop` impl called, the `Drop` impl of the promoted
@@ -111,6 +121,55 @@ it is unlikely to be the desired behavior in most cases and very likey to be con
111121
to the user. If such behavior is desired, the user can still use an explicit `static`
112122
or `const` item and refer to that.
113123

124+
*Dynamic check.* The Miri engine could dynamically check this by ensuring that
125+
the result of computing a promoted is a value that does not need dropping.
126+
127+
## `&` in `const` and `static`
128+
129+
Promotion is also responsible for making code like this work:
130+
131+
```rust
132+
const FOO: &'static i32 = {
133+
let x = &13;
134+
x
135+
};
136+
```
137+
138+
However, since this is in explicit const context, we are less strict about
139+
promotion in this situation: all function calls are promoted, not just
140+
`#[rustc_promotable]` functions:
141+
142+
```rust
143+
const fn bar() -> i32 { 42 }
144+
145+
const FOO: &'static i32 = {
146+
let x = &bar(); // this gets promoted
147+
x
148+
};
149+
```
150+
151+
However, we still do not promote *everything*; e.g., drop-checking still applies:
152+
153+
```rust
154+
const DROP: &'static Vec<u8> = {
155+
let x = &Vec::new(); //~ ERROR: temporary value dropped while borrowed
156+
x
157+
};
158+
```
159+
160+
Notice that some code involving `&` *looks* like it relies on promotion but
161+
actually does not:
162+
163+
```rust
164+
const EMPTY_BYTES: &Vec<u8> = &Vec::new(); // Ok without promotion
165+
```
166+
167+
As we have seen above, `Vec::new()` does not get promoted. And yet this
168+
compiles. Why that? The reason is that the reference obtains the lifetime of
169+
the "enclosing scope", similar to how `let x = &mut x;` creates a reference
170+
whose lifetime lasts for the enclosing scope. This is decided during MIR
171+
building already, and does not involve promotion.
172+
114173
## Open questions
115174

116175
* There is a fourth kind of CTFE failure -- resource exhaustion. What do we do

static.md

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Statics
2+
3+
Statics (`static`, `static mut`) are the simplest kind of compile-time evaluated data:
4+
* The user explicitly requested them to be evaluated at compile-time,
5+
so evaluation errors from computing the initial value of a static are no concern
6+
(in other words, [const safety](const_safety.md) is mostly not an issue).
7+
* They observably get evaluated *once*, with the result being put at some address known at run-time,
8+
so there are no fundamental restrictions on what statics can do.
9+
* The compiler checks that statics are `Sync`, justifying sharing their address across threads.
10+
* [Constants](const.md) and [promoteds](promotion.md) are not allowed to read from statics,
11+
so their final value does not have have to be [const-valid](const_safety.md#const-safety-check-on-values) in any meaningful way.
12+
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.
13+
14+
## `Drop`
15+
16+
The compiler rejects intermediate values (created and discarded during the computation of a static initializer) that implement `Drop`.
17+
The reason for this is simply that the `Drop` implementation might be non-`const fn`.
18+
This restriction can be lifted once `const impl Drop for Type` (or something similar) is supported.
19+
20+
```rust
21+
struct Foo;
22+
23+
impl Drop for Foo {
24+
fn drop(&mut self) {
25+
println!("foo dropped");
26+
}
27+
}
28+
29+
static FOOO: Foo = Foo; // Ok, drop is never run
30+
31+
// Not ok, cannot run `Foo::drop` because it's not a const fn
32+
static BAR: i32 = (Foo, 42).1;
33+
```
34+
35+
*Dynamic check.* The Miri engine dynamically checks that this is done correctly
36+
by not permitting calls of non-`const` functions.

0 commit comments

Comments
 (0)