Skip to content

Commit cb97a08

Browse files
authored
Merge pull request #6 from RalfJung/const-safety-and-promotion
Rules for const safety and promotion
2 parents 965fbeb + 4e9cb4c commit cb97a08

File tree

3 files changed

+227
-12
lines changed

3 files changed

+227
-12
lines changed

README.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ The Rust compiler runs the [MIR](https://rust-lang-nursery.github.io/rustc-guide
1818
in the [`MIR` interpreter (miri)](https://rust-lang-nursery.github.io/rustc-guide/const-eval.html),
1919
which sort of is a virtual machine using `MIR` as "bytecode".
2020

21+
## Table of Contents
22+
23+
* [Const Safety](const_safety.md)
24+
* [Promotion](const_safety.md)
25+
2126
## Related RFCs
2227

2328
### Const Promotion
@@ -62,4 +67,4 @@ even if it does not break the compilation of the current crate's dependencies.
6267
Some of these features interact. E.g.
6368

6469
* `match` + `loop` yields `while`
65-
* `panic!` + `if` + `locals` yields `assert!`
70+
* `panic!` + `if` + `locals` yields `assert!`

const_safety.md

+135-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,135 @@
1-
# Const safety
1+
# Const safety
2+
3+
The miri engine, which is used to execute code at compile time, can fail in
4+
four possible ways:
5+
6+
* The program performs an unsupported operation (e.g., calling an unimplemented
7+
intrinsics, or doing an operation that would observe the integer address of a
8+
pointer).
9+
* The program causes undefined behavior (e.g., dereferencing an out-of-bounds
10+
pointer).
11+
* The program panics (e.g., a failed bounds check).
12+
* The program loops forever, and this is detected by the loop detector. Note
13+
that this detection happens on a best-effort basis only.
14+
15+
Just like panics and non-termination are acceptable in safe run-time Rust code,
16+
we also consider these acceptable in safe compile-time Rust code. However, we
17+
would like to rule out the first two kinds of failures in safe code. Following
18+
the terminology in [this blog post], we call a program that does not fail in the
19+
first two ways *const safe*.
20+
21+
[this blog post]: https://www.ralfj.de/blog/2018/07/19/const.html
22+
23+
The goal of the const safety check, then, is to ensure that a program is const
24+
safe. What makes this tricky is that there are some operations that are safe as
25+
far as run-time Rust is concerned, but unsupported in the miri engine and hence
26+
not const safe (they fall in the first category of failures above). We call these operations *unconst*. The purpose
27+
of the following section is to explain this in more detail, before proceeding
28+
with the main definitions.
29+
30+
## Miri background
31+
32+
A very simple example of an unconst operation is
33+
```rust
34+
static S:i32 = 0;
35+
const BAD:bool = (&S as *const i32 as usize) % 16 == 0;
36+
```
37+
The modulo operation here is not supported by the miri engine because evaluating
38+
it requires knowing the actual integer address of `S`.
39+
40+
The way miri handles this is by treating pointer and integer values separately.
41+
The most primitive kind of value in miri is a `Scalar`, and a scalar is *either*
42+
a pointer (`Scalar::Ptr`) or a bunch of bits representing an integer
43+
(`Scalar::Bits`). Every value of a variable of primitive type is stored as a
44+
`Scalar`. In the code above, casting the pointer `&S` to `*const i32` and then
45+
to `usize` does not actually change the value -- we end up with a local variable
46+
of type `usize` whose value is a `Scalar::Ptr`. This is not a problem in
47+
itself, but then executing `%` on this *pointer value* is unsupported.
48+
49+
However, it does not seem appropriate to blame the `%` operation above for this
50+
failure. `%` on "normal" `usize` values (`Scalar::Bits`) is perfectly fine, just using it on
51+
values computed from pointers is an issue. Essentially, `&i32 as *const i32 as
52+
usize` is a "safe" `usize` at run-time (meaning that applying safe operations to
53+
this `usize` cannot lead to misbehavior, following terminology [suggested here])
54+
-- but the same value is *not* "safe" at compile-time, because we can cause a
55+
const safety violation by applying a safe operation (namely, `%`).
56+
57+
[suggested here]: https://www.ralfj.de/blog/2018/08/22/two-kinds-of-invariants.html
58+
59+
## Const safety check on values
60+
61+
The result of any const computation (`const`, `static`, promoteds) is subject to
62+
a "sanity check" which enforces const safety. (A sanity check is already
63+
happening, but it is not exactly checking const safety currently.) Const safety
64+
is defined as follows:
65+
66+
* Integer and floating point types are const-safe if they are a `Scalar::Bits`.
67+
This makes sure that we can run `%` and other operations without violating
68+
const safety. In particular, the value must *not* be uninitialized.
69+
* References are const-safe if they are `Scalar::Ptr` into allocated memory, and
70+
the data stored there is const-safe. (Technically, we would also like to
71+
require `&mut` to be unique and `&` to not be mutable unless there is an
72+
`UnsafeCell`, but it seems infeasible to check that.) For fat pointers, the
73+
length of a slice must be a valid `usize` and the vtable of a `dyn Trait` must
74+
be a valid vtable.
75+
* `bool` is const-safe if it is `Scalar::Bits` with a value of `0` or `1`.
76+
* `char` is const-safe if it is a valid unicode codepoint.
77+
* `()` is always const-safe.
78+
* `!` is never const-safe.
79+
* Tuples, structs, arrays and slices are const-safe if all their fields are
80+
const-safe.
81+
* Enums are const-safe if they have a valid discriminant and the fields of the
82+
active variant are const-safe.
83+
* Unions are always const-safe; the data does not matter.
84+
* `dyn Trait` is const-safe if the value is const-safe at the type indicated by
85+
the vtable.
86+
* Function pointers are const-safe if they point to an actual function. A
87+
`const fn` pointer (when/if we have those) must point to a `const fn`.
88+
89+
For example:
90+
```rust
91+
static S: i32 = 0;
92+
const BAD: usize = &S as *const i32 as usize;
93+
```
94+
Here, `S` is const-safe because `0` is a `Scalar::Bits`. However, `BAD` is *not* const-safe because it is a `Scalar::Ptr`.
95+
96+
## Const safety check on code
97+
98+
The purpose of the const safety check on code is to prohibit construction of
99+
non-const-safe values in safe code. We can allow *almost* all safe operations,
100+
except for unconst operations -- which are all related to raw pointers:
101+
Comparing raw pointers for (in)equality, converting them to integers, hashing
102+
them (including hashing references) and so on must be prohibited. Basically, we
103+
should not permit any raw pointer operations to begin with, and carefully
104+
evaluate any that we permit to make sure they are fully supported by miri and do
105+
not permit constructing non-const-safe values.
106+
107+
There should also be a mechanism akin to `unsafe` blocks to opt-in to using
108+
unconst operations. At this point, it becomes the responsibility of the
109+
programmer to preserve const safety. In particular, a *safe* `const fn` must
110+
always execute const-safely when called with const-safe arguments, and produce a
111+
const-safe result. For example, the following function is const-safe (after
112+
some extensions of the miri engine that are already implemented in miri) even
113+
though it uses raw pointer operations:
114+
```rust
115+
const fn test_eq<T>(x: &T, y: &T) -> bool {
116+
x as *const _ == y as *const _
117+
}
118+
```
119+
On the other hand, the following function is *not* const-safe and hence it is considered a bug to mark it as such:
120+
```
121+
const fn convert<T>(x: &T) -> usize {
122+
x as *const _ as usize
123+
}
124+
```
125+
126+
## Open questions
127+
128+
* Do we allow unconst operations in `unsafe` blocks, or do we have some other
129+
mechanism for opting in to them (like `unconst` blocks)?
130+
131+
* How do we communicate that the rules for safe `const fn` using unsafe code are
132+
different than the ones for "runtime" functions? The good news here is that
133+
violating the rules, at worst, leads to a compile-time error in a dependency.
134+
No UB can arise. However, thanks to [promotion](promotion.md), compile-time
135+
errors can arise even if no `const` or `static` is involved.

promotion.md

+86-10
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,96 @@
11
# Const promotion
22

3+
Note that promotion happens on the MIR, not on surface-level syntax. This is
4+
relevant when discussing e.g. handling of panics caused by overflowing
5+
arithmetic.
6+
37
## Rules
48

5-
### 1. No side effects
9+
### 1. Panics
10+
11+
Promotion is not allowed to throw away side effects. This includes
12+
panicking. Let us look at what happens when we promote `&(0_usize - 1)` in a
13+
debug build: We have to avoid erroring at compile-time (because that would be
14+
promotion breaking compilation), but we must be sure to error correctly at
15+
run-time. In the MIR, this looks roughly like
16+
17+
```
18+
_tmp1 = CheckedSub (const 0usize) (const 1usize)
19+
assert(!_tmp1.1) -> [success: bb2; unwind: ..]
20+
21+
bb2:
22+
_tmp2 = tmp1.0
23+
_res = &_tmp2
24+
```
625

7-
Promotion is not allowed to throw away side effects.
8-
This includes panicking. So in order to promote `&(0_usize - 1)`,
9-
the subtraction is thrown away and only the panic is kept.
26+
Both `_tmp1` and `_tmp2` are promoted to statics. `_tmp1` evaluates to `(~0,
27+
true)`, so the assertion will always fail at run-time. Computing `_tmp2` fails
28+
with a panic, which is thrown away -- so we have no result. In principle, we
29+
could generate any code for this because we know the code is unreachable (the
30+
assertion is going to fail). Just to be safe, we generate a call to
31+
`llvm.trap`.
32+
33+
As long as CTFE only panics when run-time code would also have panicked, this
34+
works out correctly: The MIR already contains provisions for what to do on
35+
panics (unwind edges etc.), so when CTFE panics we can generate code that
36+
hard-codes a panic to happen at run-time. In other words, *promotion relies on
37+
CTFE correctly implementing both normal program behavior and panics*. An
38+
earlier version of miri used to panic on arithmetic overflow even in release
39+
mode. This breaks promotion, because now promoting code that would work (and
40+
could not panic!) at run-time leads to a compile-time CTFE error.
1041

1142
### 2. Const safety
1243

13-
Only const safe code gets promoted. This means that promotion doesn't happen
14-
if the code does some action which, when run at compile time, either errors or
15-
produces a value that differs from runtime.
44+
We have explained what happens when evaluating a promoted panics, but what about
45+
other kinds of failure -- what about hitting an unsupported operation or
46+
undefined behavior? To make sure this does not happen, only const safe code
47+
gets promoted. The exact details for `const safety` are discussed in
48+
[here](const_safety.md).
49+
50+
An example of this would be `&(&1 as *const i32 as usize % 16 == 0)`. The actual
51+
location is not known at compile-time, so we cannot promote this. Generally, we
52+
can guarantee const-safety by not promoting when an unsafe or unconst operation
53+
is performed -- if our const safety checker is correct, that has to cover
54+
everything, so the only possible remaining failure are panics.
55+
56+
However, things get more tricky when `const` and `const fn` are involved.
57+
58+
For `const`, based on the const safety check described [here](const_safety.md),
59+
we can rely on there not being const-unsafe values in the `const`, so we should
60+
be able to promote freely. For example:
61+
62+
```rust
63+
union Foo { x: &'static i32, y: usize }
64+
const A: usize = unsafe { Foo { x: &1 }.y };
65+
const B: usize = unsafe { Foo { x: &2 }.y };
66+
let x: &bool = &(A < B);
67+
```
68+
69+
Promoting `x` would lead to a compile failure because we cannot compare pointer
70+
addresses. However, we do not even get there -- computing `A` or `B` fails with
71+
a const safety check error because these are values of type `usize` that contain
72+
a `Scalar::Ptr`.
73+
74+
For `const fn`, however, there is no way to check anything in advance. We can
75+
either just not promote, or we can move responsibility to the `const fn` and
76+
promote *if* all function arguments pass the const safety check. So,
77+
`foo(42usize)` would get promoted, but `foo(&1 as *const i32 as usize)` would
78+
not. When this call panics, compilation proceeds and we just hard-code a panic
79+
to happen as well at run-time. However, when const evaluation fails with
80+
another error (unsupported operation or undefined behavior), we have no choice
81+
but to abort compilation of a program that would have compiled fine if we would
82+
not have decided to promote. It is the responsibility of `foo` to not fail this
83+
way when working with const-safe arguments.
84+
85+
### 3. Drop
86+
87+
TODO: Fill this with information.
88+
89+
### 4. Interior Mutability
90+
91+
TODO: Fill this with information.
1692

17-
An example of this would be `&1 as *const i32 as usize == 42`. While it is highly
18-
unlikely that the address of temporary value is `42`, at runtime this could be true.
93+
## Open questions
1994

20-
The exact details for `const safety` are discussed in [here](const_safety.md).
95+
* There is a fourth kind of CTFE failure -- and endless loop being detected.
96+
What do we do when that happens while evaluating a promoted?

0 commit comments

Comments
 (0)