-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Relax const-eval restrictions #3352
Conversation
|
||
If a `const fn` is called in a runtime context, no additional restrictions are applied to it, and it may do anything a non-`const fn` can (for example, calling into FFI). | ||
|
||
A `const fn` being called in a const context will still be required to be deterministic, as this is required for type system soundness. This invariant is required by the compiler and cannot be broken, even with unsafe code. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This means that, for example, CTFE can't use host floats for anything that might be NAN, because LLVM might change the NANs when run at different times, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
# Rationale and alternatives | ||
[rationale-and-alternatives]: #rationale-and-alternatives | ||
|
||
An alternative is to keep the current rules. This is bad because the current rules are highly restrictive and don't have a significant benefit to them. With the current rules, floats cannot be used in `const fn` without significant restrictions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to see a sketch of what "significant restrictions" might actually look like. It's hard for me to weigh "not significant" vs "significant" without more details.
For example, some of the provenance discussion basically ended up at "we at least need ______ because LLVM & GCC both require that, and it's a non-starter to require a completely new optimizing codegen backend". I'd love to tease out more how bad extra things would be. "Painful but we've done things like it before", like symbolic pointer tracking, is a very different consequence from, say, "it's an open research question if it's even possible".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rust-lang/rust#77745 has a lot more info on that, I will look through it again to sketch out some concrete ideas for const floats under the current rules once I have time (unless someone else wants to do that :)).
But from what I recall:
There are really two ways to restrict this. We could try to ensure this dynamically by erroring out as soon as problematic values (like NaNs or subnormals) are produced. This should be compatible with the current rules, because even though there are differences between const and runtime, but these rules are unobservable from code. This would be a restriction, but should not be a big one for most code.
Alternatively, we could statically forbid all operations that could lead to NaNs or subnormals in const fn
. This is not realistic.
There's also the option of using softfloats at runtime inside const fn
but that's not a realistic option either.
So actually I do think that floats inside const fn
are possible under the current rules without too mnay restrictions. But this RFC still makes it simpler and obvious. (and also comes with many other benefits aside from floats).
|
||
Also, floats are currently not supported in `const fn`. This is because many different hardware implementations exhibit subtly different floating point behaviors and trying to emulate all of them correctly at compile time is close to impossible. Allowing const-eval and runtime behavior to differ will enable unrestricted floats in a const context in the future. | ||
|
||
Rust code often contains debug assertions or preconditions that must be upheld but are too expensive to check in release mode. It is desirable to also check these preconditions during constant evaluation (for example with a `debug_or_const_assert!` macro). This is unsound under the old rules, as this would be different behavior during const evaluation in release mode. This RFC allows such debug assertions to also run during constant evaluation (but does not propose this itself). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This hint at another possible alternative, to me: treating diverging differently from the results of the evaluation.
After all, it's normal that two things with the same postcondition can actually have different behaviour, if they need to diverge to communicate that they cannot uphold the promised post-condition.
(That certainly doesn't solve nondeterministic NANs, but might address a useful subset.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like diverging is always a valid possibility even under the current behaviour, since we already do this with things like overflow: const evaluation will always fail on overflow, whereas overflow might wrap at runtime. Or like, the power could go out suddenly and stop the program during runtime, but by nature of the program already being compiled, clearly it successfully computed everything during compile time.
I mean, I know that people are clearly discussing this already in cases like that, but I feel like this kind of restriction is something that could be removed even without this RFC, whereas this RFC proposes things like, f32::sin
could compute the sine in degrees in constant evaluation and radians at runtime. (Obviously that would be a horrible idea, but nonetheless possible.)
From what I remember, C++ library used the rule "possible constexpr behaviors of X must be a subset of possible runtime behaviors of X" when adding constexprs to functions. This rule covers a lot of cases starting from imprecise results with floating points, and ending with asserts as a subset of undefined behavior for functions with broken pre-requisites.
|
This feels a lot to me like the entire Drawing the connection to that discussion, I definitely think that this is something we want to do, but I also feel like there needs to be more emphasis put on safety here. I don't have any concrete ideas of how this could happen, but with To put it more clearly: I think that this RFC should be explicit about the fact that, while the behaviour may differ between const and runtime contexts, programs should not be allowed to rely on which context is being run for safety. This, in effect, would nullify this clause:
In other words, I feel like this RFC shouldn't explicitly rule out the possibility of dynamically evaluating constant values at runtime (via inlining or otherwise) or statically evaluating intermediate values at compile time (for example, simplifying mathematical expressions). The compiler won't automatically do this, of course, but it could be useful for testing purposes or so-called "risky" optimizations; these are normally called unsafe, but we explicitly prevent them from being unsafe with this clause. Note: I may just be overcomplicating this and this clause would be a bad idea. But I figure it's worth talking about in case someone has a real example of this. |
What was the reason for the limitation to begin with? |
It required additional logic to be able to differ, so the original implementation didn't. This RFC is the result of effectively years of discussions on this topic. We hope that anyone with a good use case for keeping the status quo will speak up, because so far we've only been able to create artificial examples |
No strong opinions about this, but I'd like to point out that 'different behavior at runtime is UB' and 'different behavior at runtime is fine' are not the only possible options. Another option is to say 'calls to the function nondeterministically produce either the runtime or the const-eval behavior'. In other words, the compiler is allowed to take calls to When it comes to real examples where such an optimization might be useful: look at uses of the GCC extension #define hweight8(w) (__builtin_constant_p(w) ? __const_hweight8(w) : __arch_hweight8(w)) That is, if
#define __const_hweight8(w) \
((unsigned int) \
((!!((w) & (1ULL << 0))) + \
(!!((w) & (1ULL << 1))) + \
(!!((w) & (1ULL << 2))) + \
(!!((w) & (1ULL << 3))) + \
(!!((w) & (1ULL << 4))) + \
(!!((w) & (1ULL << 5))) + \
(!!((w) & (1ULL << 6))) + \
(!!((w) & (1ULL << 7)))))
That said, one reason this works well in C is that Indeed, an alternative way to support the above use case would be to just expose |
Yet another option is to say that the language semantics and compiler don't make any assumptions, but the safety invariant of such functions guarantees that replacing the runtime version by the compile-time version is fine. (That's basically @petrochenkov's subset property, but as a safety invariant instead of a validity invariant.) All of these have in common that if we ever guarantee that certain hardware-specific aspects e.g. of floating point NaN signs are observable in Rust, then we'd have to also guarantee that CTFE behaves in that way.
FWIW this is insufficiently defined -- if the function has type This is also closely related to @comex' proposal -- by saying that we pick const or runtime code non-deterministically at runtime, we make it trivially the case that const behavior is a subset of runtime behavior. So what would that mean for functions like https://doc.rust-lang.org/nightly/std/primitive.pointer.html#method.guaranteed_eq? I guess we'd have to say that technically the runtime version is always allowed to return |
I believe that this is a stronger version of my proposal -- I say simply that you should assume this in the case where UB may occur as a result, whereas you say this might occur whenever, even if the result would not trigger UB. |
Interesting point. Possible counterargument: If the semantic difference between "best-effort On the other hand, for It's always hard to hold the door open for an optimization that's not actually being performed. |
I agree that some sort of opt-in to the "const-time weaker version" makes sense. See for example
This is useful, for example, for the slice Anyway the current guaranteed_eq docs already presuppose something like this RFC. If we don't want this RFC, we'll have to fix those docs to say that even at runtime this may return In align_to I am currently trying to improve the situation without giving too strong guarantees via rust-lang/rust#105245. It seems like that is the minimum we have to guarantee to make people actually willing to use this function. It is somewhat unclear whether having
For the sake of this argument, imagine we guaranteed it returned |
I was able to come up with a couple of examples (not sure how good). They also rely on "definately const" bounds like in the keyword-generics proposal, and that return value const-fns doesn't depend on data behind interiour mutablity. Technically they don't inherently require the runtime results to match the compile time results (just that they are deterministic), but including these restrictions allows many of the added functions (eg #![feature(const_trait_impl)]
use crate::invariant::{Inv, InvWrap};
mod invariant {
use std::marker::PhantomData;
use std::ops::{Deref};
use std::hint::unreachable_unchecked;
#[const_trait]
pub trait Inv<T> {
fn apply(val: &T) -> bool;
}
pub struct InvWrap<T, I: const Inv<T>> (T, PhantomData<I>);
impl<T, I: const Inv<T>> InvWrap<T, I> {
pub fn new(val: T) -> InvWrap<T, I>{
assert!(I::apply(&val));
InvWrap(val, PhantomData)
}
pub fn with_mut(&mut self, f: impl FnOnce(&mut T)) {
self.assume_inv();
let inner = &mut self.0;
f(inner);
assert!(I::apply(inner))
}
fn assume_inv(&self) {
if !I::apply(&self.0) {
// SAFETY self could have only been created by new() which checks I::apply(&self.0)
// self.0 is private so it can't be accessed directly
// The only way to get a mutable reference to self.0 is through with_mut
// which allow checks I::apply(&self.0) before the reference expires
// and I::apply is a const fn so it will always return the same result
unsafe { unreachable_unchecked() }
}
}
pub fn into_inner(self) -> T {
self.assume_inv();
self.0
}
}
impl<T, I: const Inv<T>> Deref for InvWrap<T, I> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.assume_inv();
&self.0
}
}
}
mod trusted_ord {
use core::cmp::Ordering;
pub unsafe trait TrustedOrd {
fn cmp(&self, other: &Self) -> Ordering;
}
#[const_trait]
pub trait TrustedOrdRep {
type Rep<'a>: TrustedOrd where Self: 'a;
fn rep(&self) -> Self::Rep<'_>;
}
// Safety:
// Since rep() is a const fn it will always return the same result,
// so T::Rep::cmp will always be passed the same inputs.
// Since it T::Rep implements TrustedOrd it's results are valid for a total order.
unsafe impl<T: const TrustedOrdRep> TrustedOrd for T {
fn cmp(&self, other: &Self) -> Ordering {
self.rep().cmp(&other.rep())
}
}
}
struct IncreasingInvariant ();
impl const Inv<(u32, u32)> for IncreasingInvariant {
fn apply(inner: &(u32, u32)) -> bool {
inner.0 <= inner.1
}
}
type IncreasingPair = InvWrap<(u32, u32), IncreasingInvariant>;
fn main() {
let mut x = IncreasingPair::new((4, 8));
x.with_mut(|p| {
p.1 /= 4;
p.0 /= 4;
});
assert_eq!(*x, (1, 2))
} |
@dewert99 could you explain a bit what is happening there and where exactly the assumption comes in? Reverse engineering that from the code is quite a bit of work.
This is definitely not true. |
Hmm, if
Sorry, the basic idea with |
This is not actually using the constness at all, afaict all calls of |
Sorry, @oli-obk you made me realize that the |
The Maybe open a zulip thread in the keywords generics stream so we can figure it out |
What are the remaining restrictions on const functions? The RFC makes it sound like writing |
This RFC only removes the restriction that const fn must behave the same at runtime and at compile time. The difference between const fn and runtime fn is still absolutely meaningful. |
Right, but what is that difference? The book calls out the fact that you can't write an rng in a const fn, but this RFC explicitly says that that would be possible, though it does say you'd only be able to do so in runtime contexts. Why is that distinction relevant though? If we let const fns do whatever, I think even I guess my question is why have any restrictions at all? |
A const fn called at const time must still return the same output for the same inputs. Even if it doesn't do that at runtime, it must still do that at const time. |
But why? What benefits does that have? Is there relevant discussion you could point me to? I can see determinism as a good argument, but you can get around that with build.rs. Also if someone wants nondeterminism in their builds, why not? |
one reason, though probably not the only one, is that const fn feeds into the type system (eg: array length), so if a const expression at const time does not have a single solution then your types get all messed up. |
Are you saying that the compiler reevaluates const fns every time it encounters them? If so, then yeah things would totally break, but it also doesn't seem that hard to cache the results within a compilation. I think this would suck for incremental compilation though since different compilation units could refer to the same const fn and be recompiled at different times. The semantics of when a const fn would be reevaluated during incremental compilation would also probably be really annoying to deal with. All of that said, maybe solving those problems would actually result in a much simpler language. I don't think it's relevant for this RFC anymore, but something to chew on for the future. |
A final(?) remark on the floating point thing: I agree with Muon, by and large. Though I think people absolutely are evaluating "interesting numerical code" in consteval already! This is in fact a standard sillybusiness in C++ with constexpr, especially for games programmers. Though they might want nondeterminism in floats too simply so that their const float evaluation can be more accurate instead of less in case they do, in fact, target thumbv7neon or whatever... But I agree with Nilstrieb that this is "not about floats". Not only Sure, exact system resources at runtime are inherently nondeterministic in many cases, but that really just makes it so much worse to be able to burn through a bonus set during consteval. We already have the complication that when LLVM elides a too-large allocation, that also can introduce a flavor of nondeterminism: a program that should have failed, doesn't, and that difference may be based on optimization level. The compile-time environment must implicitly "leak" into the program's runtime environment somehow in order to justify either of these things in a principled manner. And then there's code that would like to be |
Resource exhaustion is a bit different, that's only non-deterministic about whether const-eval succeeds at all. A lot of heap-using code still has the property "if it gives an answer, it's always the same" (at compiletime and runtime). |
That's true! That's why I left on the note of the HashMap and its randomness... such as its iteration order, despite being something you shouldn't poke at... suddenly becoming of significant impact and observability. Though I suppose const heap could simply exclude that, but as thin as libstd is, it seems kind of painful to have to exclude one of our premiere collections from code paths that could light up during both CTFE and runtime. |
I do understand the motivation and think it's probably a necessary step, but I really dislike this. With the current restriction, |
You only need a formal guarantee that comparison is pure/consistent if that condition being true is necessary in order to fulfill some As an extra note, simply std already contains the pattern for this with Footnotes
|
So what is our plan with this RFC? It looks like the lang team would prefer to have this discussion as part of a concrete motivating feature. What could that be?
To me it seems like a dedicated "safe const_eval_select" RFC would be the best option. |
Just looking at the backlinks above, this seems to solve many problems that we're having right now. Among them:
I think accepting this RFC today would have significant benefit already, even without very design-heavy work like designing the API for stable |
Not quite; only doing that plus also exposing
Is it? The issue you link just says that it uses const_eval_select, not that it uses it in a way that violates the "both cases must behave the same way" requirement. It's not clear to me why this RFC was added to the blocking list for that tracking issue, it may have been a mistake. But to add to your list, the soundness requirement in const_eval_select that this RFC aims to lift is the reason why |
Quoting from this discussion
Maybe the RFC should be re-phrased as being specifically about having a const-eval-select mechanism (without the "identical behavior" restriction) internally and using it from stable |
@rust-lang/lang would you be interested in that? |
@Nilstrieb I wouldn't hold my breadth to get an answer like this. :) There's a reason we nominate issues for them with well-prepared summary comments -- just pinging doesn't work as they are all drowning in notifications. If you have the time I'd suggest you work on rewording the RFC. I'd be happy to give feedback. Maybe do it in hackmd so collaborative editing becomes easier. The important point is to make sure that all concerns they raised last time are resolved. |
Also: Given that this already has >100 comments, making this a new RFC might be better than updating this one. But ofc if there are important points that were raised in this thread they should be carried over into the new RFC. |
…cuviper align_offset, align_to: no longer allow implementations to spuriously fail to align For a long time, we have allowed `align_offset` to fail to compute a properly aligned offset, and `align_to` to return a smaller-than-maximal "middle slice". This was done to cover the implementation of `align_offset` in const-eval and Miri. See rust-lang#62420 for more background. For about the same amount of time, this has caused confusion and surprise, where people didn't realize they have to write their code to be defensive against `align_offset` failures. Another way to put this is: the specification is effectively non-deterministic, and non-determinism is hard to test for -- in particular if the implementation everyone uses to test always produces the same reliable result, and nobody expects it to be non-deterministic to begin with. With rust-lang#117840, Miri has stopped making use of this liberty in the spec; it now always behaves like rustc. That only leaves const-eval as potential motivation for this behavior. I do not think this is sufficient motivation. Currently, none of the relevant functions are stably const: `align_offset` is unstably const, `align_to` is not const at all. I propose that if we ever want to make these const-stable, we just accept the fact that they can behave differently at compile-time vs at run-time. This is not the end of the world, and it seems to be much less surprising to programmers than unexpected non-determinism. (Related: rust-lang/rfcs#3352.) `@thomcc` has repeatedly made it clear that they strongly dislike the non-determinism in align_offset, so I expect they will support this. `@oli-obk,` what do you think? Also, whom else should we involve? The primary team responsible is clearly libs-api, so I will nominate this for them. However, allowing const-evaluated code to behave different from run-time code is t-lang territory. The thing is, this is not stabilizing anything t-lang-worthy immediately, but it still does make a decision we will be bound to: if we accept this change, then - either `align_offset`/`align_to` can never be called in const fn, - or we allow compile-time behavior to differ from run-time behavior. So I will nominate for t-lang as well, with the question being: are you okay with accepting either of these outcomes (without committing to which one, just accepting that it has to be one of them)? This closes the door to "have `align_offset` and `align_to` at compile-time and also always have compile-time behavior match run-time behavior". Closes rust-lang#62420
Rollup merge of rust-lang#121201 - RalfJung:align_offset_contract, r=cuviper align_offset, align_to: no longer allow implementations to spuriously fail to align For a long time, we have allowed `align_offset` to fail to compute a properly aligned offset, and `align_to` to return a smaller-than-maximal "middle slice". This was done to cover the implementation of `align_offset` in const-eval and Miri. See rust-lang#62420 for more background. For about the same amount of time, this has caused confusion and surprise, where people didn't realize they have to write their code to be defensive against `align_offset` failures. Another way to put this is: the specification is effectively non-deterministic, and non-determinism is hard to test for -- in particular if the implementation everyone uses to test always produces the same reliable result, and nobody expects it to be non-deterministic to begin with. With rust-lang#117840, Miri has stopped making use of this liberty in the spec; it now always behaves like rustc. That only leaves const-eval as potential motivation for this behavior. I do not think this is sufficient motivation. Currently, none of the relevant functions are stably const: `align_offset` is unstably const, `align_to` is not const at all. I propose that if we ever want to make these const-stable, we just accept the fact that they can behave differently at compile-time vs at run-time. This is not the end of the world, and it seems to be much less surprising to programmers than unexpected non-determinism. (Related: rust-lang/rfcs#3352.) `@thomcc` has repeatedly made it clear that they strongly dislike the non-determinism in align_offset, so I expect they will support this. `@oli-obk,` what do you think? Also, whom else should we involve? The primary team responsible is clearly libs-api, so I will nominate this for them. However, allowing const-evaluated code to behave different from run-time code is t-lang territory. The thing is, this is not stabilizing anything t-lang-worthy immediately, but it still does make a decision we will be bound to: if we accept this change, then - either `align_offset`/`align_to` can never be called in const fn, - or we allow compile-time behavior to differ from run-time behavior. So I will nominate for t-lang as well, with the question being: are you okay with accepting either of these outcomes (without committing to which one, just accepting that it has to be one of them)? This closes the door to "have `align_offset` and `align_to` at compile-time and also always have compile-time behavior match run-time behavior". Closes rust-lang#62420
align_offset, align_to: no longer allow implementations to spuriously fail to align For a long time, we have allowed `align_offset` to fail to compute a properly aligned offset, and `align_to` to return a smaller-than-maximal "middle slice". This was done to cover the implementation of `align_offset` in const-eval and Miri. See rust-lang/rust#62420 for more background. For about the same amount of time, this has caused confusion and surprise, where people didn't realize they have to write their code to be defensive against `align_offset` failures. Another way to put this is: the specification is effectively non-deterministic, and non-determinism is hard to test for -- in particular if the implementation everyone uses to test always produces the same reliable result, and nobody expects it to be non-deterministic to begin with. With rust-lang/rust#117840, Miri has stopped making use of this liberty in the spec; it now always behaves like rustc. That only leaves const-eval as potential motivation for this behavior. I do not think this is sufficient motivation. Currently, none of the relevant functions are stably const: `align_offset` is unstably const, `align_to` is not const at all. I propose that if we ever want to make these const-stable, we just accept the fact that they can behave differently at compile-time vs at run-time. This is not the end of the world, and it seems to be much less surprising to programmers than unexpected non-determinism. (Related: rust-lang/rfcs#3352.) `@thomcc` has repeatedly made it clear that they strongly dislike the non-determinism in align_offset, so I expect they will support this. `@oli-obk,` what do you think? Also, whom else should we involve? The primary team responsible is clearly libs-api, so I will nominate this for them. However, allowing const-evaluated code to behave different from run-time code is t-lang territory. The thing is, this is not stabilizing anything t-lang-worthy immediately, but it still does make a decision we will be bound to: if we accept this change, then - either `align_offset`/`align_to` can never be called in const fn, - or we allow compile-time behavior to differ from run-time behavior. So I will nominate for t-lang as well, with the question being: are you okay with accepting either of these outcomes (without committing to which one, just accepting that it has to be one of them)? This closes the door to "have `align_offset` and `align_to` at compile-time and also always have compile-time behavior match run-time behavior". Closes rust-lang/rust#62420
#3514 already integrates the most important part and we've just changed the internal rules for const eval select anyways to make it safe. So I'm gonna say "task failed successfully" and close the RFC. |
Note that const eval select is safe in an unstable way. An RFC like this is still required before it can be stabilized, or used by stable code in an observable way.
|
Is there presently another issue for tracking stabilization of const eval select? Or should we just wait for a prospective stabilization RFC like @RalfJung is suggesting? |
I have created a tracking issue: rust-lang/rust#124625 |
Rendered