Description
Promotion is the process of allowing let x: &'static u32 = &1;
to be valid in Rust. Technically the &1
produces a temporary local variable and references to that don't have the 'static
lifetime. But there is a check which figures out expressions that are constant and then another pass inserts an unnameable static for the value and refers to that static instead.
This was introduced in rust-lang/rfcs#1414 and I'm doing a write up for it in https://github.com/rust-rfcs/const-eval/blob/master/promotion.md
Now there are some headaches with promotion. E.g.
let x: &'static usize = &(0 - 1);
which, while being const evaluable, would cause a const eval error due to overflow.
This is fine in the above case, since the user obviously requested promotion by setting the 'static
lifetime, but
let x: &usize = &(0 - 1);
did not and would still get promoted and then error. We work around this by leaving in the debug assertions that the compiler has on integer arithmetic, which mean even though we promoted the computation to a static, all the checks are still done at runtime (unless optimized out due to guaranteed uselessness).
So everything is fine in these simple cases, it gets problematic when there's code that can't be evaluated at compile-time but which would also not panic at runtime. E.g.
union Foo { x: &'static i32, y: usize }
let x: &bool = unsafe { Foo { x: &1 }.y == Foo { x: &2 }.y };
This errors during const eval because const eval does not compare pointers. We could do pointer comparisons, and in this case it would even be fine because &1
and &2
can never be the same address, but
union Foo { x: &'static i32, y: usize }
let x: &bool = unsafe { Foo { x: &1 }.y == Foo { x: &1 }.y };
is not so clear-cut. LLVM might decide to put both &1
and the other &1
into the same static. Then the addresses would be the same. Or it would not do that, then the addresses are different. When we move to other operators, the result isn't even decideable by LLVM anymore (only the linker knows this):
union Foo { x: &'static i32, y: usize }
let x: &bool = unsafe { Foo { x: &1 }.y < Foo { x: &2 }.y };
So, the solution was to forbid promoting unions (since pointer to usize casts are already not promotable). But this just shifts the problem
union Foo { x: &'static i32, y: usize }
const A: usize = unsafe { Foo { x: &1 }.y };
const B: usize = unsafe { Foo { x: &2 }.y };
let x: &bool = &(A < B);
which accidentally works on stable 1.27 and does undefined behaviour. It is "fixed" on beta (and the fix will be in stable 1.28), but the fix is only to abort instead of UB.
We now need to figure out the concrete rules around this so we know when and where to error out, and when to not promote.
One such solution is unconst
(similar to unsafe
, but at compile-time). This means that if you do unconst
things wrongly, your compiler might produce an error during monomorphization or codegen or whenever it feels like.
Some rules for unconst
:
- any use of
unsafe
is alsounconst
- raw pointer ->
usize
casts viaas
areunconst
What does it mean when I use unconst?
- anything
unconst
will not get promoted. unconst
is not propagated past the same boundaries thatunsafe
isn't. So aconst fn
,const
orstatic
that internally usesunconst
things does not show up asunconst
, because the final value might be perfectly fine, even if the intermediate computation did dangerous things- Using
unconst
to produce aconst
orstatic
means you need to make sure the value is actually a value of that type. So if you have ausize
, that value needs to be an integer, not an address. Similar how a&u32
needs to not be0
or generally pointing anywhere but at au32
.
We already have this concept when you compute array lengths or enum variant discriminants. So there's definite precedent for this.