Relax const-eval restrictions

Rendered

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 mem::forget discussion all over again, except in this case, there's no existing safe method to do this; the only way is via const_eval_select, currently.
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 mem::forget at least, there's the added caveat that, because calling mem::forget is safe, code can't rely on things not being leaked for safety. This is relevant in things like draining iterators, which must ensure that forgetting the iterator doesn't allow the caller to observe dropped values.
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:

Pure const fn under the old rules can be seen as a simple optimization opportunity for naive optimizers, as they could just reuse constant evaluation for const fn if the argument is known at compile time, even if the function is in a runtime context. This RFC makes such an optimization impossible.

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?

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 const fns with known arguments and replace them with const-evaluated results, but it can't assume that doing so keeps the program's behavior the same.
When it comes to real examples where such an optimization might be useful: look at uses of the GCC extension __builtin_constant_p, particularly in combination with inline assembly.  For example, Linux has the following definition for a macro equivalent to u8::count_ones:
#define hweight8(w)  (__builtin_constant_p(w) ? __const_hweight8(w)  : __arch_hweight8(w))

That is, if w is known to be constant, calculate the answer using __const_hweight8, otherwise calculate it using __arch_hweight8.
__const_hweight8 is a pure C implementation, which would be (probably) slow at runtime but is fine if the compiler is constant-folding it:
#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)))))

__arch_hweight8, on the other hand, typically expands to an inline assembly block using the CPU's popcount instruction.  This is fast at runtime, but (like all inline assembly) completely opaque to the compiler and unable to be constant-folded.
That said, one reason this works well in C is that __builtin_constant_p is preserved all the way through the optimization pipeline.  It returns true not only if the argument is a language-level constant expression, but also if it's been optimized into a constant.  This is quite different from Rust const evaluation (or C++ constexpr evaluation, for that matter) which, at least for the foreseeable future, can only be evaluated earlier in the pipeline than most optimizations.
Indeed, an alternative way to support the above use case would be to just expose __builtin_constant_p itself - or rather @llvm.is.constant.i32, the LLVM intrinsic that Clang lowers __builtin_constant_p to - as a separate API.

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'.

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.

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.

FWIW this is insufficiently defined -- if the function has type const fn myfun(f: const fn(i32) -> i32), then to even talk about "set of behaviors" you need to define which set of functions f you are considering. All safe functions? All sound functions? All syntactically valid functions (including arbitrary use of unsafe)?
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 None, even though in practice it never will? Not sure if that kind of "specification trick" really helps anyone. We'd basically specify that the runtime behavior is a big overapproximation of the actual implementation -- that is technically correct, but if we learn anything from align_to it's that this leads to endless frustration. All of these proposals would make it impossible to say "at runtime, align_to will definitely return a maximally sized middle part; only at compile-time can it be the case that all elements unnecessarily end up in the head or tail".

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 const fns with known arguments and replace them with const-evaluated results, but it can't assume that doing so keeps the program's behavior the same.

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.

All of these proposals would make it impossible to have a const fn align_to documented as "at runtime, align_to will definitely return a maximally sized middle part; only at compile-time can it be the case that all elements unnecessarily end up in the head or tail".

Interesting point.
Possible counterargument: If the semantic difference between "best-effort align_to" and "exact align_to" is important, then they really should be different functions with different names; making them the same function creates a footgun.  After all, it's normally a safe refactoring practice to just blindly mark functions as const and, if they compile, assume that they do the same thing that they do at runtime.  That wouldn't work if people wrote code that used align_to but depended on the stronger semantics.  (This is especially footgunny because it's not immediately obvious why alignment can't be known at compile time.)
On the other hand, for eq and guaranteed_eq we do have two separate functions, yet I can imagine some code using, say, a.guaranteed_eq(b).unwrap().  The reasoning might be: 'At runtime we might be comparing anything, but at compile time we only compare pointers that the compiler knows are either the same or different.  If we accidentally break that rule, no big deal, it'll just be a compile error due to a const-eval panic.'  The author might then be surprised if the code started panicking at runtime.  Even without const-eval at optimization time, such code would be non-portable, risking const-eval panics under different Rust compilers or rustc versions, since guaranteed_eq's documentation explicitly refuses to guarantee it returns Some in any circumstances (at least at compile time).  But it might work in practice, at least once rustc's const-eval implementation gets a little smarter.
It's always hard to hold the door open for an optimization that's not actually being performed.

Possible counterargument: If the semantic difference between "best-effort align_to" and "exact align_to" is important, then they really should be different functions with different names; making them the same function creates a footgun.

I agree that some sort of opt-in to the "const-time weaker version" makes sense. See for example guaranteed_eq for what that could look like. However, even those functions we currently like to specify as

At runtime this function behaves like Some(self == other).

This is useful, for example, for the slice eq function which can have a fast-path. Sure, since this is just a fast-path, returning None even at runtime would technically be fine, but if the align_to experience shows anything it is that people are very paranoid about specification hedging of this sort.
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 None.
In align_to I am currently trying to improve the situation without giving too strong guarantees via https://github.com/rust-lang/rust/pull/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 guaranteed_eq with similar wording would be allowed under the policy of "compiletime code must only behave in ways that are also possible for runtime code".

Even without const-eval at optimization time, such code would be non-portable, risking const-eval panics under different Rust compilers or rustc versions, since guaranteed_eq's documentation explicitly refuses to guarantee it returns Some in any circumstances (at least at compile time).

For the sake of this argument, imagine we guaranteed it returned Some in some cases. (We totally could do that, we just haven't bothered.) So it is possible to write portable const-code that assumes that we sometimes get Some. That would make guaranteed_eq().unwrap() a bit more reasonable, no?

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

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 InvWrap::new) to themselves be const-fns.
We could still allow const_assert!(exp) without breaking these examples as long as it expanded to something like const_assert_fn(|| exp) where const_assert_fn's argument type is impl Fn() -> bool to avoid issues with const_mut_ref.
#![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.

that return value const-fns doesn't depend on data behind interiour mutablity.

This is definitely not true. Cell::get will be a const fn eventually.

This is definitely not true. Cell::get will be a const fn eventually.

Hmm, if Cell::get can become a const fn, then the other const-eval restrictions seem less useful.

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.

Sorry, the basic idea with InvWrap is to safely pass data with an invariant around in a way that helps the optimizer (eg. indexing a slice with an increasing pair could save a bounds check). Since we're using unsafe { unreachable_unchecked() } we need to ensure that I::apply(&self.0) must return true.  We know it was true when created the InvWrap was created or after it was last mutated, but need I::apply to be deterministic (otherwise eg. we could make the invariant be that some file exists, then delete the file after creating the InvWrap and assume it still exists).  Allowing InvWrap to implement Deref also requires that I::apply(&self.0) can't have its value changed by safe code given &self.0 which is why we needed the interior mutability requirement.  Making InvWrap::new a const fn would also require that I::apply give the same result at compile-time as run-time (otherwise eg. it could return true at compile-time so InvWrap::new succeeds, but then false at run-time so we reach unreachable_unchecked().

This is not actually using the constness at all, afaict all calls of apply are runtime calls. So whatever guarantees you assumed either don't exist, or, assuming you meant to run the const-eval code at runtime (you are using const Trait, not ~const Trait), that's a funky feature that we could support, but I think that this would still work for you as you'd get the const-eval code path, which modulo floating point shenanigans would still be deterministic (or at least unaffected by this PR), even if run at runtime.

Sorry, @oli-obk you made me realize that the const Trait bounds probably aren't quite what I had in mind, at using a system like https://blog.yoshuawuyts.com/const-syntax/ the bounds should probably be something like for<C> const<C> Trait meaning that the trait methods must callable at runtime and compiletime.  The reason I didn't use ~const Trait was that implementations Trait that were runtime only as long as the methods in the impl block were runtime only.

using a system like https://blog.yoshuawuyts.com/const-syntax/ the bounds should probably be something like for const Trait meaning that the trait methods must callable at runtime and compiletime. The reason I didn't use ~const Trait was that runtime-only implementations of Trait would then be allowed as long as the methods in the impl block were runtime only.

The ~const Trait syntax is just sugar for the hypothetical for<C> syntax. So if you don't mean that, then I'm unsure what feature you are describing

using a system like https://blog.yoshuawuyts.com/const-syntax/ the bounds should probably be something like for const Trait meaning that the trait methods must callable at runtime and compiletime. The reason I didn't use ~const Trait was that runtime-only implementations of Trait would then be allowed as long as the methods in the impl block were runtime only.

The ~const Trait syntax is just sugar for the hypothetical for<C> syntax. So if you don't mean that, then I'm unsure what feature you are describing

What are the remaining restrictions on const functions?
The RFC makes it sound like writing const fn would become obsolete because any function could be const. I'm assuming I've misread because that would be a huge deal and massively simplify the language. Tons of unstable features would be obsolete like const traits, for loops in const contexts, pointers, all the "make this fn const" features, etc.

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.

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 build.rs would be obsolete since you could do whatever I/O you need as part of normal compilation.
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.

if a const expression at const time does not have a single solution

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.

@dewert99

Hmm, if Cell::get can become a const fn, then the other const-eval restrictions seem less useful.

Yeah looks like the thing you want is not something const fn intends to provide. These are not "purely functional", e.g. they will also eventually be able to take and mutate &mut arguments.
@SUPERCILEX

Are you saying that the compiler reevaluates const fns every time it encounters them?

When one crate uses an array length of foo() and another crate also uses that array length, then it will be evaluated twice. Note that neither of these crates might depend on each other. Nobody has suggested a credible caching scheme so far that would reliably avoid re-evaluation. You are pulling a "just do something else" here and totally underestimate how hard it is to "just" do that.
const fn must be entirely deterministic and produce the same result when evaluated on the same arguments. That is not up for discussion in this RFC -- changing that would be a far more wide-reaching RFC, and to my knowledge no member of the relevant teams things that is feasible.

So far however it was also the case that we forced const fn to be fully deterministic at runtime, and that is what this RFC intends to change.

Thanks for the clarification/details, that makes it clear!

on the same arguments

Ohhhhh, that's the key problem. Even if the two crates depend on the same const fn, there needs to be a way to coordinate the evaluation of the function in what's currently two independent compilations.
From a conceptual standpoint, this still seems relatively easy: each crate offers a memoizing evaluation server for the const fns it declares, and dependent crates use that server for const fn evaluation from that crate (so crates B and C ask A to evaluate A's foo() for them). For incremental compilation, you need A to have serialized the evaluation to disk such that when only C is recompiled (and not B), the evaluation server loads the cached value from disk and returns that, thus being consistent with the already evaluated value B is using. But yeah, now that I understand the problem I'm sure you're totally right that "just do X" will be vastly more complicated once you dig into the details. :)

Ok yup, not possible under the rustc model. I think my idea would work for cargo since it controls the entire compilation process, but I've been playing with rustc and the kind of coordination I was talking about isn't possible as far as I can tell:

rustc lets you compile libs with independent invocations, producing an rlib file.
The spatial and temporal locality of the rlib file is unconstrained. That is, it can be copied across machines (I think) and used in a compilation years after it was produced.
Let there be a crate A and n crates A' where A' depends on A. Given point 2, A' may be compiled at an arbitrary point in time after A was compiled. Furthermore, A' may not have existed when A was compiled. Thus, A cannot know the set of inputs that will be used against its functions. Similarly given point 2, many As may exist such that A' does not know of their existence. Thus, A' cannot update the master copy of A since no such copy exists.
Assuming only forward and backwards propagation are possible, neither work so therefore you cannot coordinate universal function evaluation.

If you constrain spatial locality, then I believe back propagation should be possible, but I'm assuming that's a non-starter. Bummer, twas neat in theory.

I think it's a valuable property to say that even if code is not evaluated in a const context, it is still allowed to evaluate to the constant-evaluated value. Even if we provide guarantees that the constant-evaluated path and result is only ever actually taken if the code is actually constant-evaluated (e.g. this means we can provide guarantees like "align_to will always work properly at runtime" and "ptr::guaranteed_eq is ptr::eq at runtime"), I think we should preserve the ability for the compiler to do arbitrary MIR-level constant propagation which takes the is_constant_evaluated path.
This is still the case even if most (or even all) constant propagation done by rustc doesn't take advantage of this permission and stays in post-MIR backend passes (i.e. LLVM). On the other hand, I'd like any MIR-level constant propagation to be able to take the is_constant_evaluated branch even if it's MIR-const-propagated through non-const code.
const isn't an inlining hint, and inline-const exists to force constant evaluation where it's desired but not guaranteed. But it seems unfortunate to prohibit the compiler from taking advantage of codepaths specifically written to be constant evaluable when it has constants it could evaluate with. (Though obviously this should be limited to when context-destructive MIR opts are enabled.)
Generally, I'd expect the const algorithm to be more correct[^1] than the runtime version (where there's not a bug in one or both versions), due to a number of factors, though not in every case. Perhaps the future RFC about exposing a const_eval_select switch will need to take this into account, e.g. providing both is_const_context and is_const_evaluated separately, and perhaps even inline-!const to force the runtime path.
[^1]: For some values of correct. E.g. correct per the IEEE specification and round-to-nearest on each temporary floating point value, rather than correct to the mathimatically correct answer. Though mathematically correct but IEEE-semantic-changing transformations of float operations are potentially the most obvious use-case for differing constant evaluation behavior...
But to reiterate the point, I think it's valuable to allow and don't want to incidentally prohibit doing const evaluation where constant evaluation is not required but determined to be beneficial by compiler heuristics. I think it's possible to define guarantees such that the constant evaluated result can be used for "runtime" calculated values but only ever actually evaluated at compile time rather than runtime.

Separately and perhaps more strongly, I also think the RFC should probably explicitly say that even though unsafe code cannot rely on constant and runtime evaluation to return structurally equivalent results, it's still semantically expected that constant and runtime evaluation should do "the same thing" even if the implementations are distinct, and that it's considered valid for non-soundness-critical code to rely on const fn to be deterministic for correctness, the same way that safe code can e.g. rely on Cmp/Eq being consistent/total for correctness.
There's no language validity proof burden anymore, but the semantic expectation still exists.

Even if we provide guarantees that the constant-evaluated path and result is only ever actually taken if the code is actually constant-evaluated (e.g. this means we can provide guarantees like "align_to will always work properly at runtime" and "ptr::guaranteed_eq is ptr::eq at runtime"), I think we should preserve the ability for the compiler to do arbitrary MIR-level constant propagation which takes the is_constant_evaluated path.

Those seem like conflicting requirements to me. Either we can make such a guarantee or we can let the compiler const-prop the is_constant_evaluated path, but we can't have both. If it const-props the is_constant_evaluated path, the guarantee goes down the drain.
Also what is the benefit of doing const-prop from the is_constant_evaluated path rather than the runtime path?

const isn't an inlining hint, and inline-const exists to force constant evaluation where it's desired but not guaranteed. But it seems unfortunate to prohibit the compiler from taking advantage of codepaths specifically written to be constant evaluable when it has constants it could evaluate with.

I don't see anything unfortunate about that. If anything it helps clarify that const is completely unrelated to optimization passes such as const-prop. I think it would be a design mistake to mix up these two unrelated concepts.
const is a staging modifier, not an optimization hint. Trying to shove two orthogonal concepts into a single keyword is only going to lead to issues.
You said multiple times that you would prefer const to do both of these jobs, but why do you think that is better? Is there any concrete technical reason you can give?
The technical reasons for not doing that are quite clear IMO:

avoid entangling a priori unrelated concepts
make is_constant_evaluated safe
avoid overapproximations in specification non-determinism, where in order to follow a requirement that "const behavior must also be legal runtime behavior" we simply expand the set of permissible runtime behaviors to also include the const behaviors -- without much benefit to anyone, and with a lot of uncertainty for how to deal with this non-determinism in runtime code (see align_to)

Separately and perhaps more strongly, I also think the RFC should probably explicitly say that even though unsafe code cannot rely on constant and runtime evaluation to return structurally equivalent results, it's still semantically expected that constant and runtime evaluation should do "the same thing" even if the implementations are distinct, and that it's considered valid for non-soundness-critical code to rely on const fn to be deterministic for correctness, the same way that safe code can e.g. rely on Cmp/Eq being consistent/total for correctness.

That would make is_guaranteed_eq buggy though, wouldn't it?

(I've tried to be consistent with my use of terms here. "Constant evaluation" — the process of evaluating code at compile time. "Constant context(s)" — code which is semantically guaranteed to be constant evaluated. "Constant propagation" — the process of constant evaluation done as heuristic-guided optimization of code not in a constant context.)
This post has become entirely too long and poorly structured, so I fully accept that it might not be helpful. I've made every effort to keep it helpful, and think everything I've said is individually productive, but at this point I've tried and failed to improve the post's structure.
Having written the rambling below the fold, perhaps my request could be alternatively expressed as the four part:

I fully agree with the RFC in removing the language-level or-else-UB rule that code allowed in const contexts must evaluate to structurally equal results in constant or runtime evaluation; however
there should still be a construct like today's const_eval_select which allows the constant evaluation path to be taken outside of constant contexts if the compiler fancies, and
ideally that construct also drops the or-else-UB requirement that the choice be unobservable, instead acting as a nondeterministic choice outside of constant contexts, and
ideally, though likely as a matter of QOI rather than specification, we provide some further guarantees; e.g. if the constant evaluation path is taken, it is fully constant folded and no code is actually emitted in the executable, as if it had been put into a const block[^inline-const] in the source[^sad-const].

[^inline-const]: IIUC, we don't actually guarantee that consts are fully compile time evaluated; under as-if it's of course fully allowed for the const value to be fully reevaluated[^observed-runtime-const] each time it's used in a runtime context. The same holds for any extra redundant pure computation; technically an infinite loop could be considered a (very poor) valid compilation of any program. Not doing useless computation is a QOI concern which should have an identical (or at least very similar) QOI guarantee whether it's for arbitrary spurious computation, recomputation of const-computed values, or observable constant evaluation outside of const contexts.
[^observed-runtime-const]: Caveat: after this RFC makes const evaluation observable, such runtime evaluation would of course be required to still be structurally equivalent[^const-mut] to the const evaluation.
[^const-mut] Use of &mut (or !Freeze) allowed in const potentially makes this analysis more involved, but AIUI in order for constant evaluation including &mut to be sound, the mutable place must be entirely scoped within and not in any way escape the scope of the constant evaluation.
[^sad-const]: Reiterating, this wording still technically allows the "constant evaluation path" to translate to code in the executable. Wording a (QOI) guarantee to properly prevent this is difficult, but reasonable QOI should have heuristics that attempt to use the provided runtime evaluation path if treating the evaluated value as a static is undesirable, such as if optimizing for size and the code size is less than the static size (though optimally this is a global concern, considering the presence of other code which would force the inclusion of the runtime path (making the size cost of using it here zero) or which could also replace a static with calling the code).
That formulation a) I hope is more friendly towards both align_to and guaranteed_eq style differing behavior as well as asm! style constant-propagation-prohibiting runtime-only optimizations, and b) makes it fairly clear that this meat of this request is more for the follow-up RFC about exposing some form of is_constant_evaluated branching to user code, as I would like the RFC to distinguish between asking "is this in a constant context" and "is this in constant evaluation"[^which-query]; the former implies the latter, but the latter does not necessarily imply the former, if we allow code to observe the use of (and actually do) MIR-level constant evaluation for the purpose of constant propagation.
[^which-query]: At this point in time, I'm not really decided whether I'd prefer exposing "is_const" and "is_constant_evaluated" (the latter including constant propagation, the former only including evaluation under constant contexts) separately, or just the latter. I do think code wants some way to force that the latter is false, to sidestep code which isn't equivalent under constant evaluation (such as align_to or ptr::guaranteed_eq) and doesn't provide a version with only the runtime behavior (such as ptr::eq).
At the very end of editing this overly long post, it occurred to me that what I would like might be sufficiently provided by allowing the use of const_eval_select (or similar) outside of const functions, with the semantics of the const arm introducing a proper constant context (thus getting the constant evaluation behavior) but able to use values[^no-mut] from the surrounding context, allowing either arm to nondeterministically be taken (but as a matter of QOI, only taking the constant arm if the contextual values it uses are able to be constant evaluated, treating the arm as if an inline const block parameterized by the captured values.). I think it's reasonably likely that this is sufficient to allow what I want to allow, though I'd have to consider more if this doesn't just end up with the same problem for everything that isn't an intrinsic (and if that's even an issue). However, the rest of the post is unaware of this thought; it contains I think useful information, but I've already spent far too long editing it to completely re-edit it under this new light.
[^no-mut]: Values captured from the nonconst context into the const context should ideally be restricted such that it is possible for the const context to be self-contained, i.e. it cannot capture any references to mutable places (including by !Freeze). Whatever the ultimate const_refs_to_cell rule ends up being, this probably can reuse it, or perhaps a related rule for what values are allowed as adt const params, since those containing internal mutability is similarly problem prone.
Exposing the presence of constant evaluation outside of constant contexts heavily leans on my second request/point, that unless clearly documented otherwise, the API expectation should explicitly remain that const fn (and normal fn if/when they can specialize for constant evaluation) does "the same thing" independent of whether it is constant evaluated or not[^safe-guarantee]. Without this expectation, making constant evaluation (outside of const contexts) observable is very dangerous, as any code could be made to nondeterministically not do "the same thing" if it can be symbolically constant propagated.
[^safe-guarantee]: This is the style of API promise a safe trait provides; it on its own cannot be relied upon for soundness when calling unknown/downstream const functionality. If the const code is upstream, code may of course rely on the properties that code promises for soundness (thereby bugs in providing those properties being promoted to soundness bugs).
To that expectation, I believe

Float math clearly does "the same thing," with IEEE accuracy resulting in structural variation but semantic equivalence, at least up to compounding error.
guaranteed_eq style functions arguably do "the same thing;" Some at runtime is "just" a more accurate result than None during constant evaluation, the same as constant context IEEE operations presumably being 100% spec compliant but not guaranteeing this at runtime[^ieee] (e.g. float hardware doing flush-to-zero, the float environment being nondefault, etc).
align_to fails the "the same thing" check by any definition where "the thing" isn't nondeterministic. However, align_to's nondeterminism is simple to encapsulate back into doing "the same thing" so long as the machine code quality / perf impact of doing so is acceptable[^align_to-perf].

[^ieee]: This leads to what I think is an interesting tangential observation: allowing different constant evaluation behavior actually theoretically allows constant evaluation to be more accurate by sacrificing IEEE precision. Constant contexts obviously need to be perfectly precise, and this obviously doesn't allow non-IEEE-precise transformations outside of constant propagation, but it does allow for constant propagation to do mathematically correct but IEEE-wrong e.g. contraction that can't be allowed when feeding into the type system. Floating point operations currently aren't allowed in const fn (because of the lack of the weakening provided by this RFC, AIUI) but are allowed in computation of const items, but I wasn't able to convince myself one way or the other whether this (and constant propagation) is IEEE-precise or not.
[^align_to-perf]: In a perfect world, we can tell LLVM (or whatever codegen backend) about the runtime length limits on the prefix/suffix slices such that it knows the prefix/suffix loops only happen up to the fixed small number of times rather than the penalty of the prefix loop needing to potentially accept the entire source slice. I've not tested the behavior of LLVM here, but presumably those currently homebrewing align_to have formulations which LLVM likes (or perhaps just have clamped the handled length separately), so asking them and/or surveying their approaches would of course be useful.
I have further observations/thoughts under the fold that I couldn't find a good way to pull out to top level. There may be some repetition below the fold; my nonlinear authoring style has its drawbacks.
More rambly clarification of intent
I'm not aware of all of the intricacies of constant evaluation and propagation, so it's possible there's some incoherency in my thoughts I'm not fully cognizant of. I'm fairly certain C++ is_constant_evaluated only is true for actually consteval contexts, but as mentioned upthread __builtin_constant_p selects aware of constant propagation. To (attempt to) clarify my thoughts somewhat,
I'm not asking for __builtin_constant_p semantics, where it's aware of the compiler backend's post-MIR constant propagation; what I'd like is for the middleend working with MIR to be able to do constant propagation utilizing the same constant evaluation engine as used for constant contexts, and for that constant propagation to be allowed to utilize the more constant evaluation friendly path behind is_constant_evaluated. If it isn't, adding a runtime-optimized !is_constant_evaluated path unfortunately also serves to obstruct the efficacy of MIR-level constant propagation.
The const keyword shouldn't be (ab)used to get constant propagation, and any MIR-level constant propagation heuristics should be wholly independent of whether the code is declared to be const or not. But such constant propagation fundamentally isn't independent of whether the code is constant evaluable, because constant propagation requires doing constant evaluation. Any code matching on is_constant_evaluated is essentially saying they're providing one path which is ideal for constant evaluation and one path which is ideal for runtime execution — it seems silly to tie our hands and force constant propagation to ignore the constant evaluation friendly path that the developer has explicitly provided.
If I write a non-const function which matches on whatever is_constant_evaluated guard, can that ever take the constant evaluation path? I would like that answer to be roughly: yes; if the MIR optimizer identifies that all dependencies of the constant evaluation path are themselves constant evaluable, then the constant evaluation path may be used to create a const item to replace the entire path with, but the constant evaluation will not be taken except by the evaluation of such a replacement const, and thus will not be part of the compiled executable.
This is unfortunately very difficult (impossible?) to specify purely via operational semantics, because it's fundamentally promising a nonoperational property about whether code semantically evaluated at runtime is as an optimization actually preevaluated at compile time and furthermore allowing the code to observe this property. I'm intuitively speculating that essentially all user code is_constant_evaluated specializations are intended to be behavior-preserving, and thus that the benefit of improved constant propagation outweighs the implementation/specification costs involved in guaranteeing the properties required to confidently provide the specialized paths for constant evaluation without worrying about their presence pessimizing runtime performance.

Either we can make such a guarantee [of providing non-const-path behavior at runtime] or we can let the compiler const-prop the is_constant_evaluated path, but we can't have both. If it const-props the is_constant_evaluated path, the guarantee goes down the drain.

So yes, the actual providable guarantee isn't as strong as ideally desirable. After all, my semantics would allow promoting [0u8; 3].align_to::<u16>() to const { [0u8; 3].align_to::<u16>() } to (&[0u8; 3], &[], &[]) instead of the runtime-intended result of (&[0u8], &[0u16], &[]) or (&[], &[0u16], &[0u8]).
However, there're still potentially useful guarantees possible by refining that more lax formulation, namely that

if the constant evaluated path is taken, it is entirely folded to a single constant (i.e. as if a const block); and equivalently that
if the switched block is not constant folded (i.e. as if to a const block), the runtime evaluated path is taken.

This isn't strong enough to actually give a useful guarantee around align_to where its input is potentially constant evaluable without marking up the surrounding code (i.e. adding an is_constant_evaluated switch there as well). I do however think it's possible (though potentially quite difficult) to implement it such that align_to is not a candidate for "const promotion optimization" without also promoting the use of its result, though actually specifying such a guarantee is certainly not pretty.

You said multiple times that you would prefer const to do both of these jobs, but why do you think that is better? Is there any concrete technical reason you can give?

I don't want whether code is annotated as const to be used for constant propagation. I agree that const shouldn't be (ab)used for that purpose.
Note though, that const fn does already allow the developer to explicitly request (guaranteed possible but NOT guaranteed actually performed) constant folding by making a const item and using that, or using inline const blocks once those are stable.
What I would like to be possible is for the MIR constant evaluation capabilities of the compiler to be utilizable for MIR-level constant propagation, even (and especially) in non-const code. This doesn't require any modification to this RFC; it just requires such evaluation to use the runtime path.
What's at odds with the RFC as written is that I'd like this MIR-level constant propagation to be able to use the is_constant_evaluated guarded path; that path is by design intended to be used for MIR constant evaluation, and the runtime path is likely not compatible with MIR constant evaluation, or the switch wouldn't exist in the first place.

Also what is the benefit of doing const-prop from the is_constant_evaluated path rather than the runtime path?

The benefit is that MIR-based constant propagation can by definition fully evaluate const codepaths (if the data dependencies are also constant), but that the non-is_constant_evaluated path likely contains code which cannot be constant evaluated (otherwise why does the path split exist in the first place). As const gets more powerful the gap in functionality between stable const and unleashing the miri inside of rustc will become smaller, but there'll always be cases where MIR-level constant evaluation could be done but the APIs aren't const.
Furthermore, if is_constant_evaluated is allowed to be true outside of const fn as part of constant propagation, the constant evaluation path doesn't even necessarily need to be const. It's probably useful for the constant evaluation arm to be required const, as this ensures it's actually possible to constant evaluate it, but it must be able to move any values from the containing scope into the const context.
If whatever is_constant_evaluated functionality isn't able to use non-const values (this includes function parameters to const fn called outside of a const context), I agree that it'd be entirely useless. If today's const_eval_select is about using doing non-const things from a ~const context, I'm arguing for the ability to go the opposite direction: for the compiler to be allowed to take a non-const context and elect to turn it into a const context. This of course requires all operations within the lifted scope and all data flowing into it to be const-compatible.
__builtin_constant_p is a preexisting example where people have found it useful to provide a separate, more constant evaluation friendly path for constant propagation alongside a less constant evaluation friendly runtime-optimized path.
Perhaps this is ultimately conflating const for "this code is allowed to flow into the type system" and "this code can be evaluated at compile time" too much. I'd argue, though, that it does so no more than inline const blocks used outside of a constant context do. Technically, this does nothing; the compiler is free to instead just do the computation as written at runtime. This RFC (and not my modified form of it) gives inline const blocks not in a constant context an observable semantic meaning, as it changes the behavior of the scoped code from the runtime behavior to the constant evaluation behavior.
Inline const is exactly how I would specify the semantics of observable MIR constant propagation — promoting some code into an inline const block of the exact same code, replacing any inputs with their constant value and treating any called APIs as const (but still with a prerequisite that the constant evaluation be possible of course). The difference between RFC as-is and RFC under-my-modification is that under the former this magic "constant propagation block" doesn't put the scope into a constant context, meaning that if the constant and runtime context behaviors differ (e.g. the code uses const_eval_select or guaranteed_ style functionality), the runtime runtime semantics are used, thus preventing MIR-level constant propagation through the code if it uses guaranteed_ and it can't guarantee a Some result during constant evaluation. Under my modified version, the promoted code is actually promoted to a constant context, and thus uses the constant evaluation friendly semantics where a None from a guaranteed_ check doesn't prohibit the constant evaluation.

That would make is_guaranteed_eq buggy though, wouldn't it? We'd have to declare that it can return None even at runtime?

Presumably the worst case scenario of any code using guaranteed_eq is a pessimization, since there's no reason to use guaranteed_eq unless the code is const fn intended to be used in constant contexts.
There're perhaps two solutions here that don't require guaranteed_eq to return None at runtime, even in the face of just it and not its context getting constant propagated. (This may unfortunately contradict some of what I've said w.r.t. using align_to, which is built on top of guaranteed_ like functionality (align_offset), but at this point I've spent too long editing this brain dump for coherency already.) Namely, we could say that

guaranteed_eq returning None prevents the constant propagation entirely — it's guaranteed to have its runtime behavior of returning Some when not in a const context, though other code which switches on const evaluation can still differ in behavior and may or may not have a similar power to prevent constant evaluation propagation — or
more refined (and perhaps not actually different), that guaranteed_eq can return None in a runtime context if and only if it's in a "runtime constant context" — it's in a runtime context, but behind a is_constant_evaluated check stating that the calling context is prepared to handle the "degraded" semantics available in constant contexts. (It's potentially desirable for whatever const_select functionality to function by actually putting the constant side in a proper constant context.)

These extra semantics can happen since it's an intrinsic rather than just a library is_constant_evaluated branch. However, I've not fully fleshed out the rules I'd expect here; it's certainly not immediately self-evident.
Perhaps,
pub const fn ptr::guaranteed_eq<T>(lhs: *const T, rhs: *const T) -> Option<bool> {
    let rt = || Some(lhs.addr() == rhs.addr());
    let ct = const || {
        if intrinsics::pointers_guaranteed_equal(lhs, rhs) {
            Some(true)
        } else if intrinsics::pointee_objects_disjoint(lhs, rhs) {
            Some(false)
        } else {
            intrinsics::inhibit_const_prop();
            None
        }
    };

    match_const! {
        // if in a guaranteed const context
        const eval => ct as impl const Fn(),
        // if constant propagated
        const prop => ct as impl Fn(),
        // if runtime evaluated
        _         => rt as impl Fn(),
    }
}

I would like the semantic consistency to be called out as the default assumption, but since there's no language requirement individual functions (like guaranteed_eq) can still document that they have different semantics depending on whether they're constant evaluated, with the expectation that the caller would cover that behavioral split.
align_to is perhaps a better case study since that one is closer semantically to a library level is_constant_evaluated determination between doing the runtime alignment or not[^align_to]. It's currently the case that align_to is allowed to spuriously fail to do alignment; we'd like to have a guarantee that outside constant evaluation the alignment does happen and the pre/suffix slices are minimal[^align_to-perf].
Interestingly, even if we were to separate align_to as non-const and introduce a separate const fn guaranteed_align_to doing a similar dance as ptr::guatanteed_eq, if we do provide my vision of is_constant_evaluated in non-const functions in order to assist MIR constant evaluation driven constant propagation, align_to could potentially want to keep the "allowed to fail" semantics, preventing it from being a barrier to MIR constant propagation.
(There's also a resulting observation about guaranteed_ functions' semantics in the drop-down I can't find a good way to pull out.)
Using similar imaginary tools as previously, perhaps
const fn <[T]>::guaranteed_align_to::<U>(&self) -> (&[T], Option<(&[U], &[T])> {
    const TY_T = Layout::new::<T>();
    const TY_U = Layout::new::<U>();

    const if TY_U.is_zst() {
        // originally I wrote a bunch of code to allow transmuting ZSTs
        // technically, sufficient alignment should result in an infinite middle slice
        // with ZST in you could plausibly keep length if aligned
        // align_to with ZST is stably always (self, &[], &[]) anyway
        // None means depends on unknown runtime alignment
        // Some(&[], &[]) means guaranteed insufficient size/align
        // thus panicking seems the least wrong option here
        panic!("requested infinite slice from guaranteed_align_to");
    }

    const if TY_T.is_zst() {
        // insufficient size for non-empty middle slice
        let (prefix, rest) = self.split_at(self.len());
        let (rest, suffix) = rest.split_at(rest.len());
        let middle = transmute(rest);
        return (prefix, Some(middle, suffix));
    }

    // presume above prevents const divide by 0
    const TS_PER_U = TY_U.size() / TY_T.size();
    const_assert_eq_size!([T; TS_PER_U], U);

    // NB: returns Some(MAX) for guaranteed impossible alignment
    let Some(align_offset) = self.as_ptr().guaranteed_align_offset(TY_U.align())
    else {
        return (self, None);
    };

    let (prefix, rest) = self.split_at(align_offset.min(self.len()));
    let (rest, suffix) = rest.as_chunks::<TS_PER_U>();
    let middle = transmute(rest);
    (prefix, Some(middle, suffix))
}

fn <[T]>::align_to::<U>(&self) -> (&[T], &[U], &[T]) {
    let (this, tail) = self.split_at(self.len());
    match this.guaranteed_align_to() {
        (prefix, Some(middle, suffix)) => (prefix, middle, suffix),
        (prefix, None) => (prefix, tail, tail),
    }
}

Writing out this exact formulation for guaranteed_align_to suggests that maybe the guaranteed_ functions should actually always have the consteval semantics and return None if the answer isn't guaranteed symbolically (thus invalidating this align_to impl, requiring it to duplicate the guaranteed_align_to impl replacing the guaranteed_ usage), but that's AIUI (essentially) impossible; it requires any callers to monomorphize over whether the result is symbolically/consteval guaranteed or not. To avoid that, we must allow a nondeterministic choice between None (this is consteval and an answer depends on runtime properties) or Some (the answer is guaranteed symbolically at consteval time INCLUSIVE OR runtime properties are known and provide an answer). Although of course, after this RFC allows different behavior in constant evaluation than at runtime, the nondeterminism can be removed by guaranteeing Some at runtime and the symbolic guarantee during const evaluation.

We'd need to choose between either

align_to can "fail" at doing the alignment, resulting in overlong prefix/suffix; if you require it to not fail for correctness, you need to wrap it in a !const block to block MIR constant propagation and ensure it doesn't fail or match_const! to provide a separate implementation path which handles the aligning failing; or
align_to is guaranteed to provide minimal prefix/suffix slices, but can sometimes inhibit the efficacy of symbolic execution (MIR/Miri) based constant propagation in order to provide that guarantee. If this runtime performance / emitted code quality guarantee of align_to isn't important, consider using guaranteed_align_to() instead; the compiler will typically still be able to optimize the emitted runtime code similarly to align_to, but by removing the guarantee of alignment success, is more easily capable of completely folding away the loop when everything is constant.

The latter seems to me slightly preferable at this point in time, since users of align_to are probably using it as an optimization over just looping over &[T] directly. However, it'd require either duplicating the align_to impl (once using guaranteed_, once using the non-const versions) to support that, or some way to tell constant evaluation based constant propagation to not happen.
No matter what choice align_to makes, if something like match_const! is available to callers, callers can write separate codepaths for the runtime-guaranteed maximal middle slice and for constant evaluation if that's important for performance[^align_to-perf].
Perhaps,
fn <[T]>::align_to::<U>(&self) -> (&[T], &[U], &[T]) {
    let rt = unsafe || match self.guaranteed_align_to() {
         (prefix, Some(middle, suffix)) => (prefix, middle, suffix),
        _ => unreachable_unchecked!(),
    };

    match_const! {
        // if in a guaranteed const context
        const eval => unreachable!(),
        // if constant propagated
        const prop => unimplemented!(),
        // if runtime evaluated
        _         => rt as impl Fn(),
    }
}

I have my winter break now, so will be more available for a while; please feel free to ping me on Zulip and/or Discord if you'd like a faster throughput locale to discuss/clarify the implications of what (I think) I want.

ideally, though likely as a matter of QOI rather than specification, we provide some further guarantees; e.g. if the constant evaluation path is taken, it is fully constant folded and no code is actually emitted in the executable, as if it had been put into a const block1 in the source2.

This isn't possible in general. The function call may take runtime arguments, even if the function logic is picked to be the const version. If you want guaranteed const evaluation, that's what const blocks are for. We'll still strive to const eval them during const propagation if the arguments end up being const.

(I've tried to be consistent with my use of terms here. "Constant evaluation" — the process of evaluating code at compile time. "Constant context(s)" — code which is semantically guaranteed to be constant evaluated. "Constant propagation" — the process of constant evaluation done as heuristic-guided optimization of code not in a constant context.)

FWIW that is not whats "constant evaluation" usually means in Rust. "constant evaluation" refers strictly to the evaluation of const, static, array length, and other things like that. The optimization that is const propagation is not constant evaluation. It shares some code with the const eval interpreter (and much less so with the new proper lattice-based const prop that recently landed), but that's about it.
What you describe sounds a bit like a form of "guaranteed const propagation". (a) that has never been the goal of const, so this is a massive expansion of scope, in a direction pretty orthogonal to this RFC. (b) It also sounds really complicated, and I don't see sufficient gain over just using const blocks, which is currently the idiomatic way to express "this must be computed at compile time".

I needed some way to refer to both constant evaluation (the proper meaning / what C++ calls "manifestly constant-evaluated") and constant propagation, thus the co-opting of the term. I'll stick to other less problematic phrasing moving forward.

I agree that constant propagation is tangential to this RFC; this RFC is about allowing whether code is constant evaluated to be observable. It may be tangentially relevant, but only inasmuch as it is potentially impacted by the semantics of const fn.
The portion of my request relevant to this RFC (and which still stands) is that I would like to request that

This RFC should explicitly note that const fn are still expected to do "the same thing" whether or not they are constant evaluated unless they document that this isn't the case.

This is not strictly necessary to add in the RFC — this is just the normal expectation that applies to all documented public functionality, whether const or not, that (barring bugs) functions should document what their inputs are. This RFC adds a new "is constant evaluated" input to const functions, and as a matter of good API practice, functions should document if their behavior depends on this implicit context.
However, because this RFC is about allowing const fns' behavior to depend on this context, I think it would be beneficial to explicitly call out that it's a good idea to document when behavior does rely on it. It should IMHO be stated that deliberately changing the semantic result of a const fn based on if it is constant evaluated or not is expected to be the exception rather than the norm, even if the difference being observable (in the as-if rule sense) is comparatively common.
This is completely independent of my theoretical (ab)use of this same contextual parameter for what I'll call "elective constant promotion."
I think elective constant promotion is still interesting, but agree it's of minimal further relevance to this RFC, so I've put it below the fold, and it should probably be ignored for the purpose of this thread. (I've mostly put it there to a) directly respond to a couple related points, and b) I don't really have a better place to record my thoughts; I really should recover access to a decent blog platform for longform musings.)
Elective Constant Promotion

What you describe sounds a bit like a form of "guaranteed const propagation". (a) that has never been the goal of const, so this is a massive expansion of scope, in a direction pretty orthogonal to this RFC.

I don't want anything guaranteed. The point of what I'm picturing is specifically to allow elective constant propagation to utilize the consteval-friendly path provided by the developer when they've provided both a consteval-friendly and a consteval-difficult implementation. (E.g. the former is resilient to guaranteed_* functions returning None, but the latter requires Some.)
I agree that this is tangential to this RFC; this RFC is strictly about allowing intrinsic language functionality to expose differing behavior depending on if it is constant evaluated or not (for this purpose, the entire core library can be considered intrinsic to the language); this RFC does not yet propose any way for the developer to do so.

(b) It also sounds really complicated, and I don't see sufficient gain over just using const blocks, which is currently the idiomatic way to express "this must be computed at compile time".

Given I'm talking about electively running code at compile time, I hope the prospective benefit over just guaranteed compile time options is clear (being able to electively run more code at compile time), even if the potential benefit is actually small.
It is complicated, though, even the simplified version now described here. Having worked it out a bit more, the extra further complexity for doing implicit elective constant promotion certainly isn't worth it. (It might remove some complexities of the explicit version (e.g. flowing it back into the type system) along with adding new complexities, but at least the explicit version's complexity is localizable.)
Having considered it a bit more, I think I do agree that the cost of doing implicit elective constant promotion is in fact, not worth it. Especially given that I'm also positing as an axiom that most code shouldn't differ depending on if it's constant evaluated or not, making all code potentially constant evaluated and care if it's constant evaluated seems like a bad idea.
Since doing an implicit elective constant promotion is problematic, the likely answer is explicit elective constant promotion. Separately from const_eval_select, which is for introducing a nonconst path to const code and is guaranteed to choose the branch based on if the execution context is constant evaluated, have some elective_const_select which adds the consteval branch to the allowed runtime behaviors, such that the provided consteval-friendly implement can be used by constant propagation.
The more I bang on this idea and the better I understand it, I move very quickly towards agreeing that it's just outright not a good idea. I as of right now remain unconvinced that it's not beneficial enough to justify the (now very localizable) complexity, but it's certainly very close to the tipping point, and probably needs much more retirement to simplify it as much as possible before it's worth including. The benefit has the potential to be significant, with the obvious example being it allows implementing count_ones-using-popcnt-like optimizations in library code (without inhibiting constant propagation) instead of relying on adding intrinsics for everything ... but on the other hand, we could also just add instrinsics as necessary in the relatively rare cases when new intrinsic functionality at this level becomes available, significantly reducing the potential benefit of being capable of a library-level solution (and just optimizing better, anyway). I think elective_const_select is still potentially useful enough to pull its weight (enough to not make choices preventing it, but not enough to prioritize over basically any other feature), but in order for it to do so,
I would like elective_const_select to be specified such that the consteval arm is essentially a const block which is const-parameterized over any values it uses from its context, including restricting captured values' types to those usable as const generics. Yes, this is extremely limiting without feature(adt_const_params), by design, for the exact same reasons for that feature gate. (Ideally, also warning if it's known that some captured value can never be constant, e.g. if it comes from FFI (incl. IO).) As a matter of QOI to make elective_const_select actually useful, the compiler should attempt to

(high priority, easier) only emit backend bytecode for either the const or the nonconst arm, but not both; and
(medium priority, difficult) only use the const arm when the code is sufficiently inlined, monomorphized, and/or constant propagated such that all of its captured values can be constant promoted and the const arm can thus be monomorphized/promoted to a singular const block/item (or a heuristically sufficiently small number of cases to be worth the duplication of constant items).

Both properties are trivially provided by just never using the consteval path. Unfortunately, it seems that actually utilizing the const arm while still providing a reasonable heuristic guarantee for the latter property may be unreasonably difficult:

This isn't possible in general. The function call may take runtime arguments, even if the function logic is picked to be the const version.

The concept is that (as a matter of QOI only; it's always semantically valid to nondeterministically use either arm, and this is what makes choosing which arm to use as an optimization valid) the const arm would only be chosen after the function is determined to take constant (after optimization) arguments. (Or rather more precisely, the const arm's captured values are constant after optimization; arguments not captured are not required to be constant, and any locals which are captured are required to be constant, and the locals' (non)constantness depends on their computation alongside any data dependencies on arguments.)
This does require new MIR functionality which does not exist today in order to defer the choice/evaluation of the const arm, but I personally see no fundamental reason why it would be impossible, even to do so with perfect accuracy; as far as I can tell, it's "just" an implementation task, although a difficult one that may involve phase mixing which isn't currently necessary.
On the other hand, if the constified captures are actually used as const generics (not just by the new faux-const-generic-item shim) instead of just parameters to const fn, runtime execution of the const arm becomes significantly more difficult, perhaps mixing layers (monomorphizing more functions during optimizations‽) so much it becomes outright impossible. For this reason I suggest outright not allowing this. It of course wouldn't be possible in a scheme doing elective constant promotion implicitly, as then you don't actually have a const name to try to use as a const generic. However, I currently believe/agree that making elective optimization deliberately observable in that manner is almost certainly a bad idea.
Sketch
As far as I can tell, the major parts in MIR for implementing elective_const_select with the behavioral qualities I'd like are:

Support for (const) generic const items. This is separately desirable.
Support for using nonconst locals as const parameters of referenced const items / in the computation of const locals. (Essentially just sugar over a giant match monomorphizing for every permutation, but without actually doing that, since that'd be prohibitively expensive.)
Support for nondeterministic branching, such that later passes can choose which branch to take.
Support for hinting that choice to take the arm using the runtime-const-generic-const-item if and only if the parameters to it are const after .
Resolving the fact that this fundamentally relies on top-down inlining/optimization whereas IIRC MIR inlining/optimization is (like most other systems) primarily bottom-up (because reusing and avoiding duplicated optimization effort is significantly easier when bottom-up).

As a sketch of the translated MIR structure, the surface Rust (with made-up and hopefully sufficiently explicit syntax for surface elective_const_select functionality)
const fn f_ct(x: i32) -> i32;
fn f_rt(x: i32) -> i32;

pub fn f(x: i32) -> i32 {
    elective_const_select! {
        match x {
            const X => ct(X),
            _ => rt(x),
        }
    }
}

would translate to MIR along the lines of
// WARNING: This format is intended for human consumers only, is
// subject to change without notice, and was mangled by CAD97.

fn ct(let x: i32) -> i32 { … }
fn rt(let x: i32) -> i32 { … }

fn f(
    let x: i32,
) -> i32 {
    let mut _0: i32; // return place
    let _1: i32;

    bb0: {
        // new: switch on constness
        switchIsConst(x) -> [const: bb2, otherwise: bb1];
    }

    bb1: {
        StorageLive(_1);
        _1 = x;
        _0 = rt(move _1) -> bb3;
    }

    bb2: {
        // new: use let as const
        // new: const fn call
        _0 = const ct(const x) -> [return: bb3, unwind: bb2];
    }

    bb3: {
        return;
    }
}

Added MIR abilities/semantics for this:

switchIsConst: deferring the choice as late as possible, goto const: if the parameter has been optimized to be constant, otherwise otherwise:.

Testing nicety: can be alternatively compiled to take a runtime-randomized branch (see below).

Using let as const: assumed always (although potentially transitively) behind a switchIsConst check for the binding; the binding can be used wherever const _ can be used, with identical semantics.

Testing nicety: can be alternatively compiled using nonconst semantics (see below).

const fn call: support for const generic const items; constant evaluates the called function similarly to an outlined const _ item, but provides return:unwind: edges instead of treating unwind as const error (see below); on unwind:, suppresses the const unwind (and here uses the nonconst arm as a fallback)

Testing nicety: can be alternatively compiled using nonconst semantics (see below).

Theoretically, it could be considered marginally valid (equiv. relying on compiler impl details) to rely on the otherwise: branch being taken if the input is FFI-derived and the compiler under default settings consistently never spuriously runtime evaluates the const: branch, or to rely on the const: branch unwinding as a way to abort the const arm and get the otherwise: semantics. The presence of a testing switch to enable deliberate spurious use of the const: arm should hopefully be at least as effective at encouraging people to not rely on this as -Cpanic=abort is for people not relying on unwinding. That is to say, reasonably, but not fully.
Of course, if the captured constant is used as a const generic rather than just as a parameter to const fn, then doing runtime evaluation is of course impossible with current-day semantics. Since we have a named constant now, using it as a const generic could eventually allowed, but this is significantly more difficult, since this means potentially requesting fresh monomorphizations from the MIR optimization pass required for this constant propagation and promotion. I don't think it's impossible to allow — with a fully querified compiler, there should be no barriers to calling back into a previous phase — but the mixing of phases seems highly undesirable, and raises the danger of post-optimization errors (😰) further (as now post-monomorohization errors can occur post-optimization).

Writing that sketch made one detail I've otherwise glossed over apparent: what happens if the const arm of elective_const_select panics? This is post-monomorohization and even post-optimization (😰), so any diagnostic would only be exposed after sufficient MIR inlining/optimization — very unfortunate. It's probably desirable to opt in to treating these as errors (or at least showing warnings), although they of course suffer all the problems of post-monomorohization diagnostics, amplified. I've opted here to handle the const arm unwinding by discarding the unwind and using the runtime arm instead, but it's also a very reasonable (potentially better) idea to serialize the panic into the executable and throw it when the block is hit. It's at least superficially similar to the unconditional_panic lint.
For debugging/testing purposes, it's likely very desirable to be able to force the const: path to be taken (and runtime evaluated) in debug/test builds, and probably even to randomly choose between the two paths; doing so can help diagnose if code is improperly relying on getting one result or the other. If the const arm unwinds in testing mode, it should probably always unwind rather than fallback to the runtime arm, potentially even aborting or unwinding bypassing catch_unwind to unconditionally fail the test.
The random testing option, if provided, should probably minimally be deny-by-default if combined with a non-cfg(test) build, to strongly discourage people from using it as an entropy source. Abusing hash_map::RandomState is a better idea, and std will TBQH probably expose a getrandom API (e.g. std::os::Entropy: io::Read, anyone?) long before elective_const_select becomes a possibility.

(I've rapidly both clarified my understanding and drastically walked back what I'm asking for; I do apologize for the noise caused in order to clarify my thinking. Had I spent a couple more days drafting my thoughts I might've been able to skip straight to this post's formulation, which is significantly more coherent than my initial post, both internally, w.r.t. the RFC, and hopefully to const's design/semantics.)

Honestly, the potential footguns from this proposal are too big for me to support it. However, I’d at least be okay with allowing differences where a const function diverges in a const context but returns a value in a non-const context, as well as only imposing this restriction on safe functions.
Existing functions that might return different results in a const context that in a non-const context such as pointer::guaranteed_eq should be marked unsafe, at least when called in a const context. This would require something like unsafe(const) for functions such as slice::as_simd.
The idea of allowing floating-point operations in const as they are, however, is hopelessly broken. I propose the following design (once we get const trait impls):

Add a sealed FloatEval trait with two implementations: ConstFloatEval and RuntimeFloatEval, the former being const.
In a non-const context, raw floating-point operations are implemented as operations using RuntimeFloatEval.
In const contexts where raw floating-point operations are currently allowed, interpret them in terms of ConstFloatEval for backwards compatibility, but deprecate the use of raw floating-point operations.
(Optional) Add a macro that takes a FloatEval type and an arithmetic expression and converts it to use the operations from the FloatEval.

the potential footguns from this proposal are too big for me to support it.

Can you elaborate on the footguns you see? You mention mitigations, but none of the issues

Given
const fn f(n: usize) -> usize { /* ... */ }

imagine the following:
static DATA: [u8; f(100)] = /* ... */;

fn get_byte_of_data_with_wrapping(i: usize) -> u8 {
    // Safety: i % f(100) < f(100), but which f(100) are they?
    unsafe { *DATA.get_unchecked(i % f(100)) }
}

That being said, I’m struggling to find a case where you couldn’t avoid the unsafe code; in the example above, for instance, replacing the unsafe line with DATA[i % f(100)] produces the same code. In fact, the burden of not depending on const fns to produce the same results in both const and non-const contexts might be manageable.
On second thought from my previous comment, unsafe might not be the right way to express “this function might return different things on constness”. If that’s the case, then we could instead use an attribute named something like #[result_depends_on_constness], which would make a const function require an explicit marker to be called in a const context other than a function with the same attribute. The language could also add an allow-by-default lint (const_dependent_const_fn) to control whether #[result_depends_on_constness] functions can be called directly.

Yea, I'd imagine every case of such unsafe code would benefit perf-wise from a single constant or just grabbing the information from where it is stored (the array length in this case).
Any unsafe shenanigans that would depend on const fn being the same at runtime and compile-time would already have to trust that function to do the right thing for their use case. I'd expect that either the code doesn't pass any tests or it works just fine, but mostly you should probably have a constant instead of two function calls to the same function. That seems strictly more reviewable and less fragile in general.
The case you have brought up has been the go-to reason we have been avoiding doing this RFC, but we have not encountered any such code being written as long as const fns have been stable.
Thus this RFC: in my opinion this RFC enables many correct and useful use cases without preventing any use cases except for hypothetical ones.

Any unsafe shenanigans that would depend on const fn being the same at runtime and compile-time would already have to trust that function to do the right thing for their use case.

This would still be good to officially formulate somehow, like I mentioned before. I know that the compiler doesn't work this way, but imagining if a const expression were directly inlined into running code, not evaluated beforehand, we should still say that code can't require a particular constness of evaluation to ensure safety.

@CAD97

This RFC should explicitly note that const fn are still expected to do "the same thing" whether or not they are constant evaluated unless they document that this isn't the case.

I'm totally fine with that. This would be a constraint on the level of PartialEq functional requirements -- not to relied upon by unsafe code, but (unless documented otherwise) potentially leading to panics or other unexpected (but safe) behavior.
@bluebear94

get_byte_of_data_with_wrapping

Nice example!
However note that this would be broken even if we say that the behaviors of const fn must be a subset of the possile behaviors of that fn at runtime. The only way to make such code sound is to require that const fn, if they succeed, also behave fully deterministic at runtime. That's a pretty severe restriction.
So I agree with @oli-obk -- I think we should only bless such code if f is a known function here and guarantees to behave deterministically. Note that this RFC here only affects the case where f is unknown, e.g. because it is a closure or otherwise kept abstract. If you wrote f and ensured it is deterministic, you are fine. This RFC just means you have to check this yourself (as you would in most comparable situations of writing unsafe code), instead of being able to rely on the const.

This would still be good to officially formulate somehow, like I mentioned before. I know that the compiler doesn't work this way, but imagining if a const expression were directly inlined into running code, not evaluated beforehand, we should still say that code can't require a particular constness of evaluation to ensure safety.

No idea what a "constness of evaluation" is, but I agree we need to adjust some docs -- currently https://github.com/rust-lang/const-eval/blob/master/const.md says consts behaves as if they were inlined at every use site, which would no longer be true after this RFC.

No idea what a "constness of evaluation" is, but I agree we need to adjust some docs -- currently https://github.com/rust-lang/const-eval/blob/master/const.md says consts behaves as if they were inlined at every use site, which would no longer be true after this RFC.

So, my way of defining this is essentially defined by the const_eval_select intrinsic. Essentially, if something is evaluated at compile time, it's considered const, and if it's evaluated at runtime, it's considered non-const.
So, the proposal I'd have of strengthening this RFC is that while const expressions may have additional side effects at runtime or an entirely different value, any code that uses a const expression may not rely on exactly one of these sets of behaviour for safety.
So, simple example: let's say that we have const VAL_1: usize that is set to 1 at runtime and 0 at compile time. This is completely fine, but if I have const VAL_2: usize = unsafe { [0].get_unchecked(VAL_1) }, this would strictly be undefined behaviour even though we would normally assume that, since VAL_2 is const, this is okay.

let's say that we have const VAL_1: usize that is set to 1 at runtime and 0 at compile time

That is definitely not happening. A constant is always evaluated at compile time.

To avoid burying the lede: this RFC results that const promotion now impacts behavior, thus cannot be performed[^2] anywhere it currently isn't nor removed where it is currently performed[^3]. Thus, we should audit const promotion to ensure it is as predictable as possible (while not breaking existing reliance on const promotion (e.g. to &'static)), as the presence of spurious const promotion is no longer unobservable.
[^2]: because the const behavior is not added as a valid runtime behavior. Keeping it possible in some form is essentially what I've been discussing with "elective const promotion".
[^3]: Are there cases where we do const promotion because we can but it isn't necessary? If you write let x = &const_fn(), const promotion is required where the lifetime exceeds the lexical scope, but before this RFC unobservable if the reference isn't used outside of its lexical scope (assuming trivial drop glue). Do we still perform const promotion in this position whenever it's possible, whether the reference's scope requires it or not?

We've previously explained usage of const items as being morally equivalent to inlining the definition of the const and computing it at runtime; if a const is used as a runtime value rather than a const generic type system value, const really was from that perspective just an optimization request. There was no semantic difference between let x = const_fn(); and let x = { const X: usize = const_fn(); X };. (Avoiding inline const block for clarity.)
This RFC should ideally be explicit that this explanation of const is no longer true. Perhaps somewhat implicitly prior to this RFC (via const generics?) but as a forcing matter by this RFC, a const context (implicit promotion and const blocks) is now a guarantee that the expression is evaluated at compile time... or at least that the const semantics are provided[^1].
[^1]: It's always valid for the compiled program to do extra work, so long as the equivalent result is produced. const evaluation of floating-point using const items could be deferred to runtime (essentially, JIT const evaluation) if and only if the compiler can guarantee that the target's runtime float behavior strictly matches the strictly IEEE complying semantics used for const evaluation. If the compiler cannot make that guarantee, it must meet it somehow else. Given that it must be able to do AOT const evaluation for the purpose of const generics anyway, that's doing AOT const evaluation. And finally, if the AOT evaluation must exist and be used for some targets, it's much more simple to just guarantee the AOT evaluation is always used, and using a const item is semantically equivalent to using a structural literal for the resulting value (still subject to the standard redundant work allowance caveat), not to using the source computing expression.
This also probably means as a prerequisite to (or part of) accepting this RFC, we need to nail down what expressions get promoted into const context; generally how const promotion happens. This RFC makes const promotion observable, so it also makes const promotion a matter of behavior, not just whether code compiles.
const promotion is tricky, and inline const blocks were introduced to reduce the necessity of relying on const promotion. I can't think of examples off the top of my head, but IIRC there are positions where a value could potentially be required to be const but never undergoes const promotion; it's for these const blocks were introduced, rather than complicate const promotion rules to handle them.

Since I'm writing a reply anyway, just to clarify:

This RFC should explicitly note that const fn are still expected to do "the same thing" whether or not they are constant evaluated unless they document that this isn't the case.

I'm totally fine with that. This would be a constraint on the level of PartialEq functional requirements -- not to relied upon by unsafe code, but (unless documented otherwise) potentially leading to panics or other unexpected (but safe) behavior.

Yes, exactly; this matches my intent as well.
While behavior is allowed to be different based on if the code is being const evaluated, and it's guaranteed that you'll get the corresponding semantics, it's still a potentially surprising thing that should be used carefully.
This note should live very similarly to the note about safe trait expectations, and perhaps even alongside it, as traits are (once they're allowed to contain const functions) the only/primary way (ignoring duck-typed macros) to consume an arbitrary const fn where you just have the signature guarantee and can't soundly[^4] rely on the implementation.
[^4]: I need to put a minor further caveat on this, since I've discussed this recently. Relying on the implementation of upstream code (even std) is allowable by the language, but is relying on implementation details, thus the soundness of doing so is debatable at best. Relying on language/compiler/std implementation details is pretty universally panned as unsound (sometimes said to just be "happens to work" UB rather than differentiating the reliance on implementation details / lack of LLVM-level UB). Doing so for upstream non-std crates is slightly more of a grey area, less likely to be called UB without an "under library updates" qualifier. But even relying on behavior correctness as documented is technically relying on implementation details (the lack of bugs); you're accepting that an upstream correctness bug could cause a soundness bug in your code. This is fine and necessary for modularization of code, but at a formalization level it's important to understand that your soundness proof relies on an assumption of upstream correctness.

let's say that we have const VAL_1: usize that is set to 1 at runtime and 0 at compile time

That is definitely not happening. A constant is always evaluated at compile time.

Right, replace that with const fn val_1() -> usize. To me there's really not a semantic difference between the two in terms of the proposal at hand, but maybe I'm misled.

To me there's really not a semantic difference between the two in terms of the proposal at hand, but maybe I'm misled.

There is a difference, and it's the reason this RFC makes any sense. If you call a const fn st runtime, it will be run at runtime, there is no const evaluation happening. If you use a const at runtime, you get its value inserted right there.
We should indeed adjust the "as if pasted in" rule of constants once this RFC lands.

We should indeed adjust the "as if pasted in" explanation of constants once this RFC lands.

This is slightly off-topic here but for what it's worth the desire to use this definition has always confused me. Admitting that consts are values seems much simpler

We'll still need some form of it, for generic consts (associated const of generic trait) as we can't evaluate them to a value when used in a generic context and e.g. promoted

To me there's really not a semantic difference between the two in terms of the proposal at hand, but maybe I'm misled.

There is a difference, and it's the reason this RFC makes any sense. If you call a const fn st runtime, it will be run at runtime, there is no const evaluation happening. If you use a const at runtime, you get its value inserted right there.
We should indeed adjust the "as if pasted in" explanation of constants once this RFC lands.

Okay, I mean, I was going to say that this means it's semantically different to have an entire const fn body surrounded by a const block, and then I realised that we're making this now the case for unsafe too. Until we've fully got the story correct, it's kind of hard to keep everything consistent in your head.
Regardless, my proposal is still the same. Imagine const fn one() -> usize which returns 0 at const time and 1 at runtime. Then, const fn two() -> usize { unsafe { [0].get_unchecked(one()) } invokes undefined behaviour at runtime. But, my proposal is to consider this undefined behaviour at const time as well, even if we put const TWO: usize = two(), because if at any point we used TWO at runtime, we "could" inline the expression instead of the value, and this would invoke undefined behaviour.
The idea isn't that we're somehow going to switch to this behaviour, ever. But the idea is that, if a theoretical version of Rust chose to do this, even if the program would not run correctly, it would still be safe.
Another useful scenario which I think is less controversial is the opposite: what if we returned 1 at const time and 0 at runtime. Then, the const function is still invoking undefined behaviour even if it's never assigned to a const or put in a const block, since we could theoretically at some point in the future decide to opportunistically inline const expressions as a form of optimisation.
Again, the proposal is not to ever switch to this behaviour by default, but to ensure that any hypothetical changes would not lead to undefined behaviour. And, importantly, we can always rescind these decisions and say "actually, this is okay now" and no harm is done.

The idea isn't that we're somehow going to switch to this behaviour, ever. But the idea is that, if a theoretical version of Rust chose to do this, even if the program would not run correctly, it would still be safe.

Maybe a better argument is that users expect inlining a const to be the same as using it?

@clarfonthey I entirely don't follow at all. If a function returns 0 at const-time, I don't think we should ever force people to consider any other option at const-time. That's a lot of pain and complication for no apparent reason.
Further up the thread, the opposite has been proposed: if the function returned 0 at runtime and 1 at compiletime, then we should specify that it is also allowed to return 1 at runtime. IOW, the set of runtime behaviors must be a superset of the compiletime behaviors. There I can see the sense of it, though I still don't think it is a good idea to achieve this simply by weakening the runtime spec with a lot of non-determinism. But you are proposing the opposite, that the set of runtime behavior be a subset of the set of possible compiletime behaviors. Why?

Maybe a better argument is that users expect inlining a const to be the same as using it?

We have at least some evidence that the opposite is the case, see past discussions about accessing statics in consts where people are regularly stumped when we tell them one reason is that evaluating a const must behave as if i were inlined.

We'll still need some form of it, for generic consts (associated const of generic trait) as we can't evaluate them to a value when used in a generic context and e.g. promoted

For generic consts, I don't follow. With this change, they will also not behave like inlined.
For promoteds, ensuring they behave as if inline becomes an obligation of the analysis that creates, not the process that evaluates them.

For generic consts, I don't follow. With this change, they will also not behave like inlined.
For promoteds, ensuring they behave as if inline becomes an obligation of the analysis that creates, not the process that evaluates them.

That's why I said "some form". We have the exposed bodies rule that looks at the body of constants to determine Sync, Drop and UnsafeCell usage in the final value of the constant, even if that constant is generic and cannot be evaluated when we need this information.

We have the exposed bodies rule that looks at the body of constants to determine Sync, Drop and UnsafeCell usage in the final value of the constant, even if that constant is generic and cannot be evaluated when we need this information.

But that has nothing at all to do with inlining into runtime code? I see no relation to the current discussion. That's just static analysis of "later-stage but still compiletime" code to predict the value it will return.,

the desire to use this definition has always confused me

Well, when const A: i32 = [1][1]; was legal it was the only definition that worked, since that's not a "value".
There's also things like const EMPTY: String = String::new(); that can be used multiple times, so you need some mental model for why that's ok -- after all, it's not Copy, so how does it re-use the same value?  The "it re-runs the code to regenerate it" is an easy answer.

So, reading the comments and thinking a bit about it, I would like the proposal to instead make runtime behaviour a superset of compile-time behaviour, but I'm not 100% sure how well we could enforce that without being extremely restrictive on what users can do, which I believe is against the spirit of the RFC. Part of the the reason why I recommended adding a "but not for safety" escape hatch here is that, when it comes to entirely safe code, the RFC essentially states that you can completely differ between compile-time and runtime and that's fine.
Like, a proposal like that, to me, reads along the lines of, only code like the following is okay:
if is_runtime() {
    some_code_that_has_side_effects_but_ultimately_doesnt_affect_the_return_value();
}

Which seems a little too restrictive, at least based upon the understanding I have of the RFC's intentions. Of course, the weird bit from comes when we talk about what combinations are still allowed even in unsafe code. My recommendation of just ensuring that every combination of mixing and matching the compile-time and runtime behaviour is a bit brute-force, but I'm also very fuzzy on what a suitable alternative would look like.

So, reading the comments and thinking a bit about it, I would like the proposal to instead make runtime behaviour a superset of compile-time behaviour,

I remain unclear of what the benefits of this are. Consider the concrete case of guaranteed_eq:

Returns whether two pointers are guaranteed to be equal.
At runtime this function behaves like Some(self == other). However, in some contexts (e.g., compile-time evaluation), it is not always possible to determine equality of two pointers, so this function may spuriously return None for pointers that later actually turn out to have its equality known. But when it returns Some, the pointers’ equality is guaranteed to be known.

This function as it is currently implemented and documented violates the requirement that runtime behavior be a superset of compile-time behavior. We could fix that by saying that even at runtime, this function may return None. But what is the benefit of doing that? Note that we are not actually changing what the function does, we are purely doing some "language lawyering hack" and changing the spec to satisfy this "runtime superset of compiletime" requirement. This kind of spec-only non-determinism comes with significant cost, as we have seen in align_to: it causes confusion and uncertainty among users, and regularly leads to them entirely avoiding this function since they don't understand (and we refuse to document) when it can return None and when it will return Some.
I think there needs to be a significant practical benefit that we gain by paying this cost. And I don't see it. Sure, it is theoretically pleasing that we can now say "runtime behavior is a superset of compiletime behavior", but given that we can always achieve that with pure doc changes, without even changing the implementation, I just don't think that this has notable practical benefit. Can you name one?

Hello, I'm just jumping in from the floating-point semantics discussion. I've tried to catch up on this thread as best as I can, apologies if I retread earlier points. I don't have a stake in weakening const-eval restrictions in the broader field. However, I do want to point out that that weakening these restrictions is not necessary to make floating-point usable during const evaluation.
We currently effectively assume a specific rounding mode (rounding to nearest, ties to even). Thus, except for NaN results, the results of basic arithmetic operations (+, -, *, /, sqrt, mod, rem, fma) are already fully determined by their inputs. This covers the vast majority of floating-point usage. Behavior of transcendental functions is not mandated by IEEE 754, but could be made deterministic by fixing a particular implementation or requiring that they are correctly rounded (the latter is now recommended by the standard).
Where it gets tricky is with NaN outputs, since the specific bit patterns and their interpretation (quiet/signaling) are not fully specified. Whether or not an operation produces a NaN depends only on its arguments, but which bit pattern it produces is unspecified. Note that, however, computational operations must produce quiet NaNs.
Rust could defer on the subject of which NaNs are produced, leaving the compiler to treat them nondeterministically. At least in this instance, if const evaluation cannot compute a deterministic result, I would prefer it to fail entirely. I think it is strange if recompiling a program changes the value of a top-level static or constant, especially if the only code involved in computing it was numerical. Furthermore, producing unknown NaNs or depending on the unspecified bits of NaNs is almost certainly not what the programmer wants. In this regard, I am opposed to using weakened const-eval restrictions to allow const-evaluated floating-point code to silently produce nonsense values.

Not that we currently support (at tier2) platforms which are not IEEE754-compliant, for example ARMv7 floating point has DNZ and FTZ bits enabled in the fpenv by default, effectively disabling the existence of denormal floats.
It's fairly common for people to toggle these off, re-enabling normal floating point semantics (which I think is reasonable to do), but it isn't something we require, nor is it really realistic for us to do on the users behalf on every call that may be in to rust.
So I'm unsure taking the stance that the floating point environment (which is larger in practice than rounding modes) is fixed is really realistic, although it's... a big problem, since we use the LLVM instructions that claim the user isn't doing such things.

Users giving out callbacks to foreign code which will interfere with the floating-point environment should ensure it is restored before any environment-dependent floating-point operations are performed on the Rust side.

It's not just callbacks; you would have to toggle the floating-point environment in every extern "C" fn exported from a Rust library to C code.  (Even if it were just callbacks, it would still be practically unviable if you wanted to preserve the original C floating-point environment when calling into C.  Not doing that would risk misbehavior on the C side.)
Besides that, it doesn't make sense to force Rust to use a floating-point mode that may come with a substantial performance penalty compared to what C is using.

I would very much like Rust's floating-point arithmetic to become reproducible.

You may be interested in this old thread about a different barrier to reproducibility...

It's not just callbacks; you would have to toggle the floating-point environment in every extern "C" fn exported from a Rust library to C code. (Even if it were just callbacks, it would still be practically unviable if you wanted to preserve the original C floating-point environment when calling into C. Not doing that would risk misbehavior on the C side.)

As I said, it's an ABI issue. For extern "C" functions, we should indeed restore the floating-point environment before returning to the caller. In any case, correctness trumps performance, and the performance cost of incurring a floating-point pipeline stall on entry and exit is negligible. Furthermore this cost is only paid on platforms which are non-compliant by default, and only needs to be paid in functions which contain floating-point operations somewhere in their call graph anyway. If users really, truly want to opt out of reproducible arithmetic for the sake of speed, it could perhaps be configurable in some way.

Besides, it doesn't make sense to force Rust to use a floating-point mode that may come with a substantial performance penalty compared to what C is using.

Just because C compilers are doing it doesn't make it correct.

However, I do want to point out that that weakening these restrictions is not necessary to make floating-point usable during const evaluation.

Yeah, it's not required, true -- e.g. one alternative is to say that Rust float behavior exactly matches the target hardware semantics, and make sure that const-eval complies with that (either by bailing out on NaN or by actually implementing the right bit patterns).
But currently it seems very unlikely that those will be the Rust float semantics. It's going to be the wasm semantics at best, I assume, maybe even more permissive than that -- with a bunch of non-determinism for NaN-producing operations. And then we do have to answer the question for what the requirements are to make such operations const. We have (at least) the following choices:

potentially non-det operations cannot be const ever period.
potentially non-det operations can be const but const-eval has to bail out if non-det ever occurs.
potentially non-det operations can be const, but their const version has to behave deterministically in a way that would also be legal behavior for runtime code
potentially non-det operations can be const, but their const version has to behave deterministically -- in a way that might or might not be one of the possible behaviors at runtime. The documentation of such const operations should clearly point out the potential mismatch with runtime behavior.

Note that making the const-eval behavior non-det is not an option for soundness reasons. (In principle we could re-architect the entire approach to const-eval to maybe make this sound, but nobody has a good motivation or strong interest in that.)
This RFC proposes option 4.
For floating point operations, if we go with a runtime semantics of "when a NaN is produced, we can have an arbitrary sign and payload", the current implementation is compatible with options 3 and 4. Same for if we use the wasm semantics (at least it should be fairly easy to make const-eval compatible with that). The more we tighten our runtime semantics to match the target hardware it runs on, the harder it will be to keep things compatible with option 3.

To be clear, I don't want Rust's default compile-time FP behavior to be "match the hardware". I want it to be "do no more than what the spec says; and if the final result is not determined, fail". That is, I do want nondeterministic NaNs, but I don't want an unspecified branch of execution to be implicitly picked for me at compile time, even if it is consistent. We can't do anything about runtime NaN generation being underspecified, but in the very least we can warn the user if it happens at compile time.

Note that making the const-eval behavior non-det is not an option for soundness reasons. (In principle we could re-architect the entire approach to const-eval to maybe make this sound, but nobody has a good motivation or strong interest in that.)

I am not sure what you mean by making const-eval behavior itself nondeterministic. My thought would be to handle nondeterminism by overapproximating using abstract interpretation, but I don't know enough about the current implementation of const-eval to know how feasible this would be. I understand that some overapproximation already happens currently with pointers during const-eval?

To be clear, I don't want Rust's default compile-time FP behavior to be "match the hardware". I want it to be "do no more than what the spec says; and if the final result is not determined, fail".

So that would be option 2 in my list, I think.

We can't do anything about runtime NaN generation being underspecified, but in the very least we can warn the user if it happens at compile time.

What is the benefit of that though? Note that "fail" does not mean "warn the user", it means "refuse to build their program". If the goal merely is to warn, then we can have a best-effort diagnostic lint that is fired when a NaN is produced -- that's a totally different discussion from saying that const-eval must abort when the result of a computation is not deterministic.

I am not sure what you mean by making const-eval behavior itself nondeterministic. My thought would be to handle nondeterminism by overapproximating using abstract interpretation, but I don't know enough about the current implementation of const-eval to know how feasible this would be. I understand that some overapproximation already happens currently with pointers during const-eval?

Yeah we kind of do something like that for pointers, but it's a huge amount of work and honestly I don't think doing the same for NaNs is worth the effort.

Yeah we kind of do something like that for pointers, but it's a huge amount of work and honestly I don't think doing the same for NaNs is worth the effort.

One option that seems plausible to me is to have a warning if the bitwise representation of a NaN is ever "exposed" during compilation, regardless of whether the NaN was generated in a non-deterministic way. Presumably this would be simpler since you don't need an abstract representation for floats, you can just look to see if it's a NaN.

What is the benefit of that though? Note that "fail" does not mean "warn the user", it means "refuse to build their program". If the goal merely is to warn, then we can have a best-effort diagnostic lint that is fired when a NaN is produced -- that's a totally different discussion from saying that const-eval must fail and stop compilation when the result of a computation is not deterministic.

The benefit is that people writing numerical code never actually want a NaN. If we const-eval or constant fold something to NaN, it was almost certainly unintentional. People who really do want NaNs should pick a NaN. Sorry if I was unclear, I didn't mean "warn" as in "issue a warning diagnostic".

One option that seems plausible to me is to have a warning if the bitwise representation of a NaN is ever "exposed" during compilation, regardless of whether the NaN was generated in a non-deterministic way. Presumably this would be simpler since you don't need an abstract representation for floats, you can just look to see if it's a NaN.

That actually sounds pretty good. The main thing that would overlook is inspecting bits of fixed NaNs, but it should otherwise cover it. An error would be preferable though, in a similar way that we currently error out if we try to cast a pointer to an integer.

The benefit is that people writing numerical code never actually want a NaN.

I highly doubt interesting numerical code will run during const-eval. That would be more of an argument to add a NAN detection mode to Miri or similar sanitizers.

const-eval or constant fold

constant-folding (often also called constant propagation) is entirely unrelated to const-eval as far as the specification goes, and also not relevant to this RFC. This RFC is about const-eval only (as in, the evaluation of const, static, array lengths, enum discriminants, and const generic parameters).

it was almost certainly unintentional.

Except for the NAN constant I guess, which is deliberately defined as a NAN. ;)

One option that seems plausible to me is to have a warning if the bitwise representation of a NaN is ever "exposed" during compilation, regardless of whether the NaN was generated in a non-deterministic way. Presumably this would be simpler since you don't need an abstract representation for floats, you can just look to see if it's a NaN.

It's unclear what precisely that means, since when we load a floating point value we don't know if that will "expose" the NAN, and when we load a u32 value (e.g. in f32::to_bits) we cannot know if that is originally some float data being re-interpreted or not.
The best we can do is to add const-only assertions in f32::to_bits, but we cannot do anything if people transmute floats to ints themselves.
Also "exposing" NAN bits is very different from "producing" them, so @Muon you are asking for two completely different things in the two parts of your post.
Anyway keep in mind this RFC is not about float specifically, but about a more general question:
If const-eval (and by extension, a const fn) produces a given result, must it be guarnateed that runtime code would always produce the exact same result when evaluating the same code? If yes, this means we can never allow const-eval to evaluate anything that is non-deterministic at runtime. We also cannot have operations like guaranteed_eq or const_eval_select (a way for code to test whether it runs at compiletime or runtime). This rules out so many useful operations that I don't think we should do this. Read the RFC for details. :)
If we don't have rustc make such a gaurantee, we could still

say it is UB to violate the guarantee (but this UB is pretty hard to define operationally so that's probably a bad idea)
say it is unsound to violate the guarantee, i.e., public safe functions must uphold the guarantee but unsafe code can violate it internally
say it is sound to violate the guarantee but there is an expectation that this will be clearly pointed out in the const fn documentation

It is also worth pointing out that yo my knowledge, no other language attempts to provide such a guarantee (e.g. C++ has floats at compile-time and I don't think they make any attempts to have its NAN behavior exactly match the runtime one -- also C++ has a way to just test "is this const-eval'd" which can introduce arbitrary differences in behavior), and I am not aware that this would be a big issue in practice. So I think those people that want to severely restrict the language to not permit differences between compiletime and runtime behavior (with the consequence that some C++ features will never be available in Rust) have to justify why this is a cost worth paying. "Producing NANs is probably a bug and we want to force the compiler to detect it but only for the tiny subset of numeric code that runs at compile-time" does not sound like a particularly strong justification to me.

Is compile-time behavior diverging from runtime behavior (for which NANs are but an example) truly so bad that we must fundamentally rule it out entirely?

Even if the face of desires to abuse mixing constant evaluation (const items/generics) and constant propagation (optimization), which sits somewhere between very problematic and completely impractical, I agree that it should not be UB for constant evaluation and runtime execution behavior to be different.
It is absolutely still surprising for constant evaluation and runtime execution behavior to diverge. There is an argument to saying such divergence is unsound, but I will argue that this is a misuse of unsafe. unsafe is only supposed to be used where incorrect usage of the unsafe function either directly causes UB or causes otherwise safe APIs to become unsound when combined with the incorrect usage. A function returning a different value doesn't meet either of these conditions; it's functionally equivalent to a regular runtime function containing randomness in its behavior.
It's important for any function to document its behavior, such that downstream code can use it. unsafely trusting the behavior of an API to be what you're expecting doesn't make the API unsound if it doesn't do what you're expecting (here, changing behavior based on an input you didn't expect it to read, namely whether we're in constant evaluation). It means you have a soundness bug, but the unexpected behavior was lifted to a soundness issue when you wrote unsafe, not because upstream had a bug or did something unexpected in a normal function (i.e. not a member of an unsafe trait).
If the purpose was just to allow varying IEEE floating point behaviors, it would perhaps be simpler to say that constant evaluation's behavior is always a valid runtime behavior. (It's probably the case for float either way, because constant propagation of floating point is done with one set of semantics, which may be different from your target's float implementation.)
But it's not just about floating point. We want to be able to const_eval_select in the standard library and provide separate paths optimized separately for constant evaluation or runtime execution. Obviously, when used as an optimization/specialization, we intend for the paths to be identical. But "or else UB" is a significantly higher burden of proof than for correctness.
As far as I can tell, making behavior divergence UB is not useful as a justification for optimization[^1]; it's just as a matter of making code more predictable for the human users. And we don't need the weapon of UB for the human; we just need to be clear about conventional expectations.
Rust's unsafe model is beneficial because it allows drawing attention to places that need "or else UB" levels of scrutiny, because errors there could make debugging lie and cause all of the other UB hazards. This is beneficial because things which aren't unsafe don't have that burden of scrutiny; errors are still problems, but they're bugs which are possible to debug because they don't cause fundamental axioms to suddenly become false. We might want a "constant evaluation debugger," but behavior divergence should be a debuggable behavior, not UB.
[^1]: The one exception is utilizing constant evaluation paths for the purpose of constant propagation. I still want the developer to be able to provide a separate path which can be more constant propagation friendly than the standard runtime execution path, but this is complicated, and can easily be a distinct thing from the version of const_eval_select which doesn't convey that responsibility.

I highly doubt interesting numerical code will run during const-eval.

By that reasoning, it doesn't really matter what we do for const-eval of floating-point operations. Why not take the most conservative route with it, then? It would be trivial.

constant-folding (often also called constant propagation) is entirely unrelated to const-eval as far as the specification goes, and also not relevant to this RFC. This RFC is about const-eval only (as in, the evaluation of const, static, array lengths, enum discriminants, and const generic parameters).

I am aware. I read the RFC. I am simply pointing out that we should help the user by discouraging them from generating underspecified bit patterns, even in other parts of the compiler.

Except for the NAN constant I guess, which is deliberately defined as a NAN. ;)

That constant ought to be defined as a specific NaN using std::f32::from_bits(). Otherwise, it's not really constant, is it?

One option that seems plausible to me is to have a warning if the bitwise representation of a NaN is ever "exposed" during compilation, regardless of whether the NaN was generated in a non-deterministic way. Presumably this would be simpler since you don't need an abstract representation for floats, you can just look to see if it's a NaN.

Also "exposing" NAN bits is very different from "producing" them, so @Muon you are asking for two completely different things in the two parts of your post.

A NaN produced as a result of const-eval, that is, as a const, static, or const generic parameter, is exposed. You can call to_bits() on it, and get something out. If this is allowed, that something should not be allowed to change. The following program should not be allowed to both (1) compile and (2) produce different outputs on different versions of the compiler:
fn main() {
  const X: f32 = f(); // f returns a NaN, but does not use anything like const_eval_select
  println!("{}", X.to_bits());
}

Similarly for the following:
fn main() {
  const X: f32 = f(); // f returns a NaN, but does not use anything like const_eval_select
  if X.is_sign_positive() {
    println!("+NaN");
  } else {
    println!("-NaN");
  }
}

Anyway keep in mind this RFC is not about float specifically, but about a more general question: If const-eval (and by extension, a const fn) produces a given result, must it be guaranteed that runtime code would always produce the exact same result when evaluating the same code?

As I said earlier, I don't have a stake in this in general. However, I take issue with using it to implement unnecessarily permissive const-eval semantics for floating-point. I do not believe "allowing this allows us to implement floating-point in const-eval" is a good motivation, when it can be implemented both usefully and target-independently without it. My preference is to do more where it's possible, which is why I suggested using abstract interpretation. However, if that is too hard, then outright forbidding generation of unspecified NaNs during const-eval is my next preference. We can always expand the scope of floating-point const-eval later.

So I think those people that want to severely restrict the language to not permit differences between compiletime and runtime behavior (with the consequence that some C++ features will never be available in Rust, features that people are already requesting today) have to justify why this is a cost worth paying. "Producing NANs is probably a bug and we want to force the compiler to detect it but only for the tiny subset of numeric code that runs at compile-time" does not sound like a particularly strong justification to me.

I am only opposed to implementing floating-point const-eval using compile-time nondeterminism. As you said, only a tiny subset of numeric code runs at compile-time. What harm is there in (at least for now) forbidding the generation of unspecified NaNs?

Maybe we should instead treat const eval as a wasm VM and any value return should be serialized into some internal format and then deserialized back for the actual target.
That would fix the issue regarding float or even Vec<T> and other containers.

@NobodyXu The issue is not what code can be evaluated, but what we decide to allow. The constant evaluation engine is already capable of doing essentially whatever; the flag to remove all const restrictions is IIRC -Z unleash-the-miri-inside-of-you.
Whatever method would be used to marshal values back from wasm-powered const evaluation can just as well be used for miri-powered const evaluation.

Thanks for the explanation @CAD97

Also, a const fn called at runtime can do anything a normal function can do... call a system randomness API

Please note that this is already possible in stable safe Rust. :)
const fn get_random_uuid() -> &'static str {
    include_str!("/proc/sys/kernel/random/uuid")
}

const A: &str = get_random_uuid();

This call is not evaluated during constant evaluation but during macro expansion instead. It does not have anything to do with constant evaluation. For all the const eval system cares about, the user just wrote something in their source and modifies it on each compilation.

I don't think this is necessary. For any function that may be inconsistent with the runtime, we can implement it through soft algorithm simulation.
For floating point computing, we only need to implement a soft float-point library for it, and then users can understand that its behavior is always determined, and the results may be different from the target platform. If users do not want this difference, they will handle it by themselves.

I will add the suggested wording by Ralf later, but for now I'd like to clarify the motivation for this RFC (partly in response to @823984418's message above but I've heard similar things in other places). I'll also add this to the motivation section in some way later.
It is indeed true that there are alternative ways to solve the const float problem and that this RFC is not strictly needed for all solutions, only for some. It might make more sense to delay the decision on this until consensus on const floats is reached.
But that's not fully relevant, because const floats are, while part of it, not the primary motivation of this RFC. I originally got inspired by @thomcc's complaints about const_eval_selects safety contract being hard to verify in the standard library and I've since had more thoughts on it.
The primary motivation of this RFC for me is to get rid of uncheckable UB with dubious benefits in const_eval_select. const_eval_select is just an intrinsic, but I am of the opinion that it (or rather, something like it. const_eval_select is currently terrible ergonomics wise) should be stabilized in the future to give everyone access to do branching on whether the code is executed in const eval. This will make it way easier to make all kinds of highly optimized functions const, without relying on things like making all SIMD intrinsics or even inline assembly const evaluatable.
If we expose this in current Rust, it would have to be unsafe. Misusing it would be undefined behavior. With this it could be safe.
But why is it so terrible to have this be undefined behavior? Rust has undefined behavior in lots of places already. But I am of the opinion that each undefined behavior has a high cost. It makes code using it incorrect and completely unpredictable, so the programmer must ensure it doesn't happen. Therefore, Rust always has to strongly justify its undefined behavior. For most UB in Rust, we justify it with the current or clearly visible potential future optimization benefit. Even then, it can be a hard sell. But luckily for prorgammers, lots of UB in Rust is actually checkable. Sanitizers and Miri can be run on many (but not all) programs and verify the absence of UB for a particular execution. The undefined behavior for const_eval_select though is uncheckable. Theoretically, both versions should work at runtime so tests can be made switching between the two but even this is not nearly as exhaustive as the UB checks offered by sanitizers (which is not exactly exhaustive either, but pretty good in practice usually). It can be impossible to know that undefined behavior exists, making this extremely high cost. And the benefits to justify it? They are, in my opinion, tiny to non-existent. As already mentioned in the RFC, there have been some thoughts around caching and purity, but those are an abuse of const fn in my opinion. @CAD97 has brought up a valid point around const propagation of things like inline assembly, but I think this use case is better served by a function to branch on whether const propagation is happening (this would need to be unsafe, but since it has a clear use case and would be very rarely needed and uses should be very isolated, it would be fine).
What I see here is a very high cost of undefined behavior compared to small to non-existent benefits. We should get rid of it. That this also helps float is great and makes the argument stronger, but even without floats, I am strongly in favor of getting rid of this UB and demoting it to a simple logic error (in most cases, some might desire actual visible differences).

I agree with most of what @Nilstrieb wrote. (I am not sure about the const propagation thing, but that is a separate topic anyway.)