---
For some time now (since the 1.26 release, to be precise), Rust has a [very powerful machinery for CTFE](https://github.com/rust-lang/rust/pull/46882), or compile-time function evaluation.
---
For some time now (since the 1.26 release, to be precise), Rust has a [very powerful machinery for CTFE](https://github.com/rust-lang/rust/pull/46882), or compile-time function evaluation.
-Since then, there have been various discussions about which operations should be allowed during CTFE, which checks the compiler should do, and which kinds of guarantees we should be able to expect around CTFE.
-This post is my take on those topics, and it should not be surprising that I am going to take a very type-system centric view. :)
+Since then, there have been various discussions about which operations should be allowed during CTFE, which checks the compiler should do, how this all relates to promotion and which kinds of guarantees we should be able to expect around CTFE.
+This post is my take on those topics, and it should not be surprising that I am going to take a very type-system centric view.
The `...` here is going to be Rust code that must be "run" at compile-time, because it can be used as a constant in the code -- for example, it can be used for array lengths.
The `...` here is going to be Rust code that must be "run" at compile-time, because it can be used as a constant in the code -- for example, it can be used for array lengths.
Being an optimization, constant propagation must, by definition, not change program behavior and will not be observable at all (other than performance).
CTFE, on the other hand, is about code that *must* be executed at compile-time because the compiler needs to know its result to proceed -- for example, it needs to know the size of an array to compute how to lay out data in memory.
You can statically see, just from the syntax of the code, whether CTFE applies to some piece of code or not:
CTFE is only used in places like the value of a `const` or the length of an array.
{% highlight rust %}
fn demo() {
Being an optimization, constant propagation must, by definition, not change program behavior and will not be observable at all (other than performance).
CTFE, on the other hand, is about code that *must* be executed at compile-time because the compiler needs to know its result to proceed -- for example, it needs to know the size of an array to compute how to lay out data in memory.
You can statically see, just from the syntax of the code, whether CTFE applies to some piece of code or not:
CTFE is only used in places like the value of a `const` or the length of an array.
{% highlight rust %}
fn demo() {
Not all operations can be used in const context.
For example, it makes no sense to compute your array length as "please go read that file from disk and compute something" -- we can't know what will be on the disk when the program actually runs.
Not all operations can be used in const context.
For example, it makes no sense to compute your array length as "please go read that file from disk and compute something" -- we can't know what will be on the disk when the program actually runs.
-We could use the disk of the machine compiling the program, but that does not sound very appearling either.
-In fact, it would also be grossly unsafe:
+We could use the disk of the machine compiling the program, but that does not sound very appealing either.
+Things get even worse when you consider letting the program send information to the network.
+Clearly, we don't want CTFE to have actually observable side-effects outside of compilation.
+
+In fact, just naively letting programs read files would also be grossly unsafe:
Why should `==` or `%` not be const-safe?
Well, we could call our function as follows:
{% highlight rust %}
Why should `==` or `%` not be const-safe?
Well, we could call our function as follows:
{% highlight rust %}
However, we want to blame the `as usize` for this issue, not the `is_eight_mod_256`.
The solution is for the const type system to not just have separate rules about which operations are allowed, we also must change our notion of which values are "valid" for a given type.
However, we want to blame the `as usize` for this issue, not the `is_eight_mod_256`.
The solution is for the const type system to not just have separate rules about which operations are allowed, we also must change our notion of which values are "valid" for a given type.
I think it is possible to achieve CTFE correctness for all other operations, and I think we should strive to do so.
Before we go on, notice that CTFE correctness as defined above does not say anything about the case where CTFE fails with an error, e.g. because of an unsupported operation.
I think it is possible to achieve CTFE correctness for all other operations, and I think we should strive to do so.
Before we go on, notice that CTFE correctness as defined above does not say anything about the case where CTFE fails with an error, e.g. because of an unsupported operation.
-That is a deliberate choice because it lets us gradually improve the operations supported by CTFE, but it is a choice that not everyone might agree with.
+CTFE would be trivially correct (in the above sense) if it just always immediately returned an error.
+However, since const-safe programs cannot error during CTFE, we know from CTFE correctness that *those* programs *do* in fact behave exactly the same at compile-time and at run-time.
We have to manually ensure that, *if* our inputs are const-valid, then we will not trigger a CTFE error and return a const-valid result.
For this example, the reason no CTFE error can arise is that references cannot dangle.
We can thus provide `ptr_eq` as an abstraction that is entirely safe to use in const context, even though it contains a potentially const-unsafe operation.
We have to manually ensure that, *if* our inputs are const-valid, then we will not trigger a CTFE error and return a const-valid result.
For this example, the reason no CTFE error can arise is that references cannot dangle.
We can thus provide `ptr_eq` as an abstraction that is entirely safe to use in const context, even though it contains a potentially const-unsafe operation.
-This is, again, in perfect analogy with types like `Vec` being entirely safe to use from safe Rust even though `Vec` internally uses plenty of potentially unsafe operations.
+This is, again, in perfect analogy with types like [`Vec`](https://doc.rust-lang.org/stable/std/vec/struct.Vec.html) being entirely safe to use from safe Rust even though `Vec` internally uses plenty of potentially unsafe operations.
Whenever I said above that some operation must be rejected by the const type system, what that really means is that the operation should be unsafe in const context.
Even pointer-to-integer casts can be used internally in const-safe code, for example to pack additional bits into the aligned part of a pointer in a perfectly deterministic way.
Whenever I said above that some operation must be rejected by the const type system, what that really means is that the operation should be unsafe in const context.
Even pointer-to-integer casts can be used internally in const-safe code, for example to pack additional bits into the aligned part of a pointer in a perfectly deterministic way.
I propose to only ever promote values that are *safely* const-well-typed.
(So, we will not promote values involving const-unsafe operations even when we are in an unsafe block.)
When there are function calls, the function must be a safe `const fn` and all arguments, again, const-well-typed.
I propose to only ever promote values that are *safely* const-well-typed.
(So, we will not promote values involving const-unsafe operations even when we are in an unsafe block.)
When there are function calls, the function must be a safe `const fn` and all arguments, again, const-well-typed.
As usual for type systems, this is an entirely local analysis that does not look into other functions' bodies.
Assuming our const type system is sound, the only way we could possibly have a CTFE error from promotion is when there is a safe `const fn` with an unsound `unsafe` block.
As usual for type systems, this is an entirely local analysis that does not look into other functions' bodies.
Assuming our const type system is sound, the only way we could possibly have a CTFE error from promotion is when there is a safe `const fn` with an unsound `unsafe` block.
## Conclusion
I have discussed the notions of CTFE determinism and CTFE correctness (which are properties of a CTFE engine like miri), as well as const safety (property of a piece of code) and const soundness (property of a type system).
## Conclusion
I have discussed the notions of CTFE determinism and CTFE correctness (which are properties of a CTFE engine like miri), as well as const safety (property of a piece of code) and const soundness (property of a type system).
There are still plenty of open questions, in particular around the interaction of [`const fn` and traits](https://github.com/rust-lang/rust/issues/24111#issuecomment-311029471), but I hope this terminology is useful when having those discussions.
Let the type systems guide us :)
There are still plenty of open questions, in particular around the interaction of [`const fn` and traits](https://github.com/rust-lang/rust/issues/24111#issuecomment-311029471), but I hope this terminology is useful when having those discussions.
Let the type systems guide us :)
-Thanks to @oli-obk for feedback on a draft of this post.
-<!-- If you have feedback or questions, [let's discuss in the internals forum]()! -->
+Thanks to @oli-obk for feedback on a draft of this post, and to @centril for interesting discussion in #rust-lang that triggered me into developing these ideas and terminology.
+If you have feedback or questions, [let's discuss in the internals forum](https://internals.rust-lang.org/t/thoughts-on-compile-time-function-evaluation-and-type-systems/8004)!