Skip to content

Commit 8cea06a

Browse files
committed
const safety and promotion
1 parent 965fbeb commit 8cea06a

File tree

3 files changed

+181
-11
lines changed

3 files changed

+181
-11
lines changed

README.md

Lines changed: 6 additions & 1 deletion
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

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

promotion.md

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,52 @@
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

59
### 1. No side effects
610

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.
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)`:
13+
In the MIR, this looks roughly like
14+
```
15+
_tmp1 = CheckedSub (const 0usize) (const 1usize)
16+
assert(!_tmp1.1) -> [success: bb2; unwind: ..]
17+
18+
bb2:
19+
_tmp2 = tmp1.0
20+
_res = &_tmp2
21+
```
22+
Both `_tmp1` and `_tmp2` are promoted to statics. `_tmp1` evaluates to `(~0,
23+
true)`, so the assertion will always fail at run-time. Computing `_tmp2` fails
24+
with a panic, which is thrown away -- so we have no result. In principle, we
25+
could generate any code for this because we know the code is unreachable (the
26+
assertion is going to fail). Just to be safe, we generate a call to
27+
`llvm.trap`.
1028

1129
### 2. Const safety
1230

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.
31+
Only const safe code gets promoted. The exact details for `const safety` are
32+
discussed in [here](const_safety.md).
33+
34+
An example of this would be `&(&1 as *const i32 as usize % 16 == 0)`. The actual
35+
location is not known at compile-time, so we cannot promote this. Generally, we
36+
can guarantee const-safety by not promoting when an unsafe or unconst operation
37+
is performed. However, things get more tricky when `const` and `const fn` are
38+
involved.
1639

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.
40+
For `const`, based on the const safety check described [here](const_safety.md),
41+
we can rely on there not being const-unsafe values in the `const`, so we should
42+
be able to promote freely.
1943

20-
The exact details for `const safety` are discussed in [here](const_safety.md).
44+
For `const fn`, there is no way to check anything in advance. We can either
45+
just not promote, or we can move responsibility to the `const fn` and promote
46+
*if* all function arguments pass the const safety check. So, `foo(42usize)`
47+
would get promoted, but `foo(&1 as *const i32 as usize)` would not. When this
48+
call panics, compilation proceeds and we just hard-code a panic to happen as
49+
well at run-time. However, when const evaluation fails with another error, we
50+
have no choice but to abort compilation of a program that would have compiled
51+
fine if we would not have decided to promote. It is the responsibility of `foo`
52+
to not fail this way when working with const-safe arguments.

0 commit comments

Comments
 (0)