Thoughts on Compile-Time Function Evaluation and Type Systems


For a whereas now (for the rationale that 1.26 liberate, to be actual), Rust has a very grand machinery for CTFE, or assemble-time feature overview.
Since then, there had been varied discussions about which operations must peaceful be allowed in the future of CTFE, which checks the compiler must peaceful attain, how this all pertains to promotion and which varieties of ensures we must peaceful be ready to place a query to round CTFE.
This publish is my use on those issues, and it can well presumably peaceful now not be gorgeous that I toddle to use a extremely sort-arrangement centric scrutinize.
Request something fancy a structured brain dump, so there are some unanswered questions in direction of the tip as effectively.

Some Background

CTFE is the mechanism extinct by the compiler, essentially, to contemplate objects fancy const x: T = ...;.
The ... right here goes to be Rust code that must peaceful be “stir” at assemble-time, because it can well presumably also additionally be extinct as a fixed in the code – as an illustration, it can well presumably also additionally be extinct for array lengths.

Stumble on that CTFE is now not the same as fixed propagation: Constant propagation is an optimization pass accomplished by compilers fancy LLVM that can opportunistically alternate code fancy 3 + 4 into 7 to lead certain of stir-time work.
Being an optimization, fixed propagation must, by definition, now not alternate program habits and would possibly well presumably now not be observable the least bit (aside from performance).
CTFE, alternatively, is ready code that must be completed at assemble-time since the compiler needs to understand its consequence to proceed – as an illustration, it needs to understand the scale of an array to compute how one can lay out knowledge in memory.
You would also statically peep, precise from the syntax of the code, whether CTFE applies to some piece of code or now not:
CTFE is most efficient extinct in places fancy the associated fee of a const or the length of an array.

fn demo() {
  const X:  u32 = 3 + 4; // CTFE
  let x:  u32 = 4 + 3; // no CTFE (but perchance fixed propagation)

We command that the 3 + 4 above is in const context and hence field to CTFE, but the 4 + 3 is now not.

Const Security

No longer all operations would possibly well presumably also additionally be extinct in const context.
As an instance, it will not be wise to compute your array length as “please toddle read that file from disk and compute something” – we can’t know what will be on the disk when the program in fact runs.
We would possibly well presumably also employ the disk of the machine compiling the program, but that doesn’t sound very keen either.
Things procure even worse if you occur to contemplate about letting the program send knowledge to the network.
Clearly, we don’t need CTFE to possess in fact observable aspect-effects launch air of compilation.

No doubt, precise naively letting purposes read recordsdata would also be grossly unsafe:
When computing the length of an array twice, it’s mandatory that we create the same consequence.
Replace: As @eddyb aspects out, issues procure even worse as soon as you utilize into memoir const generics, traits, and coherence: At that point, or now not it would possibly perchance probably possess to be vital to depend on evaluating the same expression in totally different crates to procure the same consequence. /Replace

CTFE must peaceful be deterministic.

If now not, the compiler would possibly well presumably also stop up thinking that two arrays possess the same length, but then later compute totally different layouts.
That would possibly well presumably be a catastrophe.
So, to any extent further or much less exterior input and to any extent further or much less non-determinism is an entire no-toddle for CTFE.
This doesn’t precise ache I/O, even converting a reference to a usize is now not deterministic.

The compiler will throw a CTFE error if such an operation is ever tried to be completed.
These purposes that are executable in const context are known as const exact:

A program is const exact if it can well presumably also additionally be completed by CTFE with out hitting an error (panics are allowed).

Here is amazingly worthy in analogy with the premise that a exact (or stir-time exact, to repeat aside it from const exact) program is a program that doesn’t goal any memory errors or knowledge races.
No doubt, we can peep that this analogy between “purposes which would possibly well presumably be effectively-behaved below CTFE” (const security) and “purposes that attain now not goal UB” (stir-time security) can elevate us very far.

One very keen demand now is whether or now not some given feature foo must peaceful be allowed to be known as in const context.
We would possibly well presumably also precise the least bit times command “certain”, and depend on the fact that CTFE will throw an error when foo does anything fishy.
The distress with this arrangement is that, if foo is in a library, updating the library would possibly well presumably also alternate foo in a means that makes it now not const-exact.
In other words, making any feature now not const-exact to any extent further would be a semver violation because it can well presumably also rupture downstream crates.

The same old mechanism to resolve that distress is to possess an annotation that explicitly marks a feature as “usable in const context”.
In Rust, the proposed mechanism for this goal is const fn; in C++ it is is named constexpr.
The compiler can now reject calling non-const functions in const context, so library authors can add non-const-exact operations with out breaking semver.

Const Form Machine and Const Soundness

This leads us to the keen field that the compiler will reject code in const context that it can well presumably receive precise lovely launch air of const context.
In specific, the physique of a const fn is also opinion to be to be in const context – in another case, if we allowed calling arbitrary functions, we would possess the same distress again.
One important formulation to enlighten of this is that now we possess a 2d sort arrangement, a “const sort arrangement”, that is extinct to sort-take a look at code in const context.
This kind arrangement doesn’t enable calls to non-const functions.

It’ll peaceful potentially also now not enable casting a reference to an integer, because (as discussed above) that is a non-deterministic operation which would possibly’t be performed in the future of CTFE.
What else?

Sooner than we toddle on and add random extra checks, enable us to step encourage and possess a study what our targets are right here.
Veritably, the goal of a kind arrangement is to get some create of guarantee for a effectively-typed program.
For Rust’s “predominant” (“stir-time”) sort arrangement, that guarantee is “no undefined habits”, which arrangement no memory errors and no knowledge races.
What is the guarantee for our recent const sort arrangement?
Now we possess already talked about it above: It’s const security!
This leads us to the definition of const soundness:

Our const sort arrangement is sound if effectively-typed purposes are const-exact.

Once more, explore how this is amazingly same to the correctness snort for the stir-time sort arrangement, which ensures stir-time security.

Nevertheless, now we must be somewhat cautious right here.
Dispose of in thoughts the next piece of code:

const fn is_eight_mod_256(x:  usize) -> bool { x % 256 == 8 }

We can indubitably are seeking to enable this code.
Why must peaceful == or % now not be const-exact?
Well, we would possibly well presumably also name our feature as follows:

is_eight_mod_256(Box:: into_raw(Box:: recent(0)) as usize);

That snort is no doubt now not const-exact because the final consequence is reckoning on the place precisely the allocator puts our Box.
Nevertheless, we’re seeking to blame the as usize for this field, now not the is_eight_mod_256.

The resolution is for the const sort arrangement to now not precise possess separate tips on which operations are allowed, we also must alternate our idea of which values are “legitimate” for a given sort.
An integer got from a pointer is legitimate for usize at stir-time, but it with out a doubt is now not legitimate for usize in const mode!
Despite all the pieces, there are general arithmetic operations that we place a query to all usize to toughen, that CTFE cannot toughen for pointers.

A feature is const-exact if, when completed with const-legitimate arguments, it doesn’t arrangement off a CTFE error and returns a const-legitimate consequence (if it returns the least bit).

Below this definition, is_eight_mod_256 is const-exact because each time x is an actual integer, this would possibly occasionally well even overview with none error.
At the same time, this reveals that converting a reference into usize is now not const-exact, since the input of this operation is const-legitimate, but the output is now not!
This gives a exact justification for rejecting such casts in const context.

CTFE correctness

In Rust, CTFE is performed by miri, a MIR interpreter that extinct to be a separate challenge but whose core engine has been integrated into rustc.
miri will attain the code in const context step-by-step and precise complain and fail with an error when an operation can’t be performed.
This doesn’t precise ache non-determinism; miri doesn’t toughen all the pieces it can well presumably also toughen because @oli-obk is enormous cautious about now not unintentionally stabilizing habits that must undergo an RFC.

No doubt, precise now miri will reject all operations on raw pointers.
They all elevate a CTFE error and hence must all be rejected by the const sort arrangement.
The realizing is to alternate miri so as that it’ll toughen extra operations, but now we must be cautious in doing so.
I in actuality possess already talked about that miri must peaceful be deterministic, but there would possibly be one more whisper use into memoir that it’s probably you’ll well even need expected to play a worthy extra prominent feature:
CTFE, no decrease than if it succeeds, must peaceful match stir-time habits!

CTFE is moral if, when it loops eternally, completes with a consequence, or panics, that habits matches the stir-time habits of the same code.

We clearly attain now not need code to behave differently when it lives in const context and is stir by CTFE, and when it is compiled to machine-code and completed “for actual”.

Or, will we?
Don’t procure me execrable, I’m now not advocating for intentionally breaking that property, but it with out a doubt certain is fee passionate about what would toddle execrable if miri became now not CTFE-moral.
Perhaps surprisingly, evidently this would possibly occasionally now not be a soundness field!
All we care about for the goal of soundness is for CTFE to be deterministic, as already discussed.
We don’t re-stir the same code at stir-time and depend on it peaceful doing the same, so nothing in fact breaks if CTFE habits diverges from stir-time habits.

That talked about, now not being CTFE moral is no doubt very gorgeous and we must peaceful steer certain of it most efficient we can.
Nevertheless, I’m told that in fact predicting the final consequence of floating-point operations deterministically is amazingly laborious and LLVM isn’t precisely serving to.
So, we can probably possess to are dwelling with either passionate about floating point operations to be const-unsafe (elevating a CTFE error), or now not having CTFE correctness when floating point operations are enthusiastic.
I believe it is imaginable to form CTFE correctness for all other operations, and I believe we must peaceful strive to attain so.

Sooner than we toddle on, explore that CTFE correctness as outlined above doesn’t command anything regarding the case the place CTFE fails with an error, e.g. on memoir of an unsupported operation.
CTFE would be trivially moral (in the above sense) if it precise the least bit times straight away returned an error.
Nevertheless, since const-exact purposes cannot error in the future of CTFE, all people is aware of from CTFE correctness that those purposes attain in actuality behave precisely the same at assemble-time and at stir-time.

Unsafe Blocks in Const Context

Let’s command we’re seeking to lengthen miri to toughen extra operations on raw pointers.
We know now we must be cautious about keeping miri deterministic, and about declaring CTFE correctness.
Which operations will we be ready to toughen?

Stumble on that at this point, const soundness and the connected const security are now not a ache yet.
These tips come into play after we’re altering the const sort arrangement to enable extra operations.
CTFE determinism and correctness, on the other hand, are properties of the CTFE engine (miri) itself.

Enable us to peep at the next instance:

const fn make_a_bool() -> bool {
    let x = Box:: recent(0);
    let x_ptr = &*x as *const i32;
    let y = Box:: recent(0);
    let y_ptr = &*y as *const i32;
    x_ptr == y_ptr

At stir-time, whether this option returns moral or false is reckoning on whether or now not the allocator re-extinct the train for x when allocating y.
Nevertheless, on account of CTFE being deterministic, now we possess to grab one concrete reply at assemble-time, and that would possibly well presumably also now not be the precise reply.
Therefore we cannot enable this program to attain below CTFE if we’re seeking to aid CTFE correctness.
Supporting memory allocation in a deterministic formulation is perfectly probably (in actuality, miri already has that implemented), and casting a reference to a raw pointer adjustments nothing but the kind.
Essentially the most keen in fact problematic operation right here is discovering out two raw pointers for equality:
Because no doubt one of the pointers is dangling, we can now not deterministically predict the final consequence of this comparison!

In other words, if/when miri learns how one can compare pointers, we must procure it elevate a CTFE error if no doubt one of the pointers is dangling (aspects to unallocated memory), or else we would violate CTFE correctness.

Now, enable us to toddle one level up and peep at the const sort arrangement.
Now we possess considered that evaluating raw pointers can elevate a CTFE error, so this is in general now not a const-exact operation.
Same to casting tricks to integers, now we possess to procure the const sort arrangement reject code that compares raw pointers.
Nevertheless, it seems fancy a shame to now not even enable evaluating two references for equality, after converting them to raw pointers!
Despite all the pieces, references never dangle, so this would possibly be a perfectly const-exact operation.

Lucky enough, Rust already has an reply to the necessity to aspect-step the kind arrangement in controlled methods: unsafe blocks.
Comparing raw pointers is now not allowed by the const sort arrangement because it will not be const-exact, but precise fancy we enable stir-time-unsafe operations to be performed in unsafe blocks, we can enable const-unsafe operations as effectively.
So, we must peaceful be ready to write down the next:

const fn ptr_eq<T>(x:  &T, y:  &T) -> bool {
    unsafe { x as *const _ == y as *const _ }

As neatly-liked when writing unsafe code, now we must be cautious to now not violate the safety ensures which would possibly well presumably be in most cases upheld by the kind arrangement.
Now we possess to manually procure particular that, if our inputs are const-legitimate, then we is now not going to arrangement off a CTFE error and return a const-legitimate consequence.
For this instance, the goal no CTFE error can come up is that references cannot dangle.
We can thus present ptr_eq as an abstraction that is entirely exact to employ in const context, despite the fact that it comprises a potentially const-unsafe operation.
Here is, again, in perfect analogy with kinds fancy Vec being entirely exact to employ from exact Rust despite the fact that Vec internally makes employ of a range of potentially unsafe operations.

Every time I talked about above that some operation must peaceful be rejected by the const sort arrangement, what that in fact arrangement is that the operation must peaceful be unsafe in const context.
Even pointer-to-integer casts would possibly well presumably also additionally be extinct internally in const-exact code, as an illustration to pack extra bits into the aligned fragment of a pointer in a perfectly deterministic formulation.

There would possibly be one extra aspect to CTFE in Rust that I in actuality possess now not yet touched on: Promotion of static values.
Here is the mechanism that makes the next code effectively-typed:

fn make_a_3() -> &'static i32 { &3 }

This is able to well peep fancy it can well presumably peaceful be rejected because we’re returning a reference to a locally created value with lifetime 'static, but instead, Magic (TM) goes on.
The compiler determines that 3 is a static value that would possibly well presumably also additionally be computed at assemble-time and place into static memory (fancy a static variable), and hence &3 can possess lifetime 'static.
This also works with e.g. &(3+4).
Static variables, fancy const, are computed at assemble time and hence CTFE comes into play.

The basically recent aspect to this is that the particular person did now not demand for the associated fee to be promoted.
Which arrangement now we must be in fact cautious when deciding which values to promote: If we endorse something that miri cannot overview, there will be a CTFE error and now we possess precise broken compilation for no moral goal.
We better procure particular that that we most efficient promote values that we can place a query to miri to in fact be ready to compute – i.e., we must peaceful most efficient promote the outcomes of const-exact code.
You potentially already guessed it, but what I’m proposing is to employ the const sort arrangement for that goal.
Const soundness already says that this would possibly be a means to procure particular const security.

I point out to most efficient ever promote values which would possibly well presumably be safely const-effectively-typed.
(So, we is now not going to promote values gripping const-unsafe operations even after we’re in an unsafe block.)
When there are feature calls, the feature must peaceful be a exact const fn and all arguments, again, const-effectively-typed.
As an instance, &is_eight_mod_256(13) would be promoted but &is_eight_mod_256(Box::into_raw(Box::recent(0)) as usize) would now not.
As neatly-liked for sort systems, this would possibly be a thoroughly local diagnosis that doesn’t peep into other functions’ bodies.
Assuming our const sort arrangement is sound, the most keen formulation we would possibly well presumably also presumably possess a CTFE error from promotion is when there would possibly be a exact const fn with an unsound unsafe block.

Notably, we’re counting on library authors neatly writing unsafe const fn even for non-public functions each time there would possibly be one more that this option would possibly well presumably also goal a CTFE error for const-legitimate inputs.
If there would possibly be a const fn that is in general unsafe, arguing “but it with out a doubt is non-public and hence that’s lovely” is now not going to aid since the compiler would possibly well presumably also procure to promote the final consequence of that feature.
Nevertheless, this would possibly occasionally well even most efficient rupture code in the future of the same crate and would possibly well presumably additionally be fastened locally, so it seems fancy an cheap compromise to me.

Yet another keen whisper use into memoir is that we potentially care worthy extra about CTFE correctness when passionate about promotion.
Despite all the pieces, the particular person asked for the stir-time habits; if they are getting a very totally different habits from CTFE, that can lead to concerns.
If miri is CTFE-moral excluding for vague floating point disorders, which arrangement “most efficient” folks counting on specific habits of floating point operations will be affected, and certain LLVM will already violate no topic assumptions those folks are making.
(miri’s floating point implementation is perfectly sane and would possibly well presumably be requirements compliant, LLVM and the particularities of x87 rounding are the sources of uncertainty right here.)
I’m now not certain which make that must or can possess for promotion.


I in actuality possess discussed the notions of CTFE determinism and CTFE correctness (which would possibly well presumably be properties of a CTFE engine fancy miri), to boot to const security (property of a chunk of code) and const soundness (property of a kind arrangement).
In specific, I point out that when sort-checking exact code in const context, we guarantee that this code is const-exact, i.e., that this would possibly occasionally well even now not hit a CTFE error (though panics are allowed, precise fancy they are in “stir-time” Rust code).

There are peaceful a range of launch questions, in specific actual thru the interplay of const fn and traits, but I am hoping this terminology is important when having those discussions.
Let the kind systems manual us 🙂

Thanks to @oli-obk for feedback on a draft of this publish, and to @centril for keen dialogue in #rust-lang that brought about me into increasing the following tips and terminology.
Within the event you possess feedback or questions, let’s discuss in the internals forum!

Read More

Leave A Reply

Your email address will not be published.