rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
98.18k stars 12.7k forks source link

Tracking issue for comparing raw pointers in constants #53020

Open oli-obk opened 6 years ago

oli-obk commented 6 years ago

Comparing raw pointers in constants is forbidden (hard error) in const contexts. The const_compare_raw_pointers feature gate enables the guaranteed_eq and guaranteed_ne methods on raw pointers. The root problem with pointer comparisons at compile time is that in many cases we can't know for sure whether two pointers are equal or inequal. While it's fairly obvious that a pointer to a static and a local variable won't be equal, we can't ever tell whether two local variables are equal (their memory may alias at runtime) or whether any pointer is equal to a specific integer. In order to make it obvious that there is a discrepancy between runtime and compile-time, the guaranteed_* methods only return true when we can be sure that two things are (in)equal. At runtime these methods just return the result of the actual pointer comparison.

This permits const fn to do performance optimization tricks like the one done in slice comparison (if length is equal and pointer is equal, don't compare the slice contents, just return that it's equal).

Since we aren't sure yet what the semantics of functions that return different values at runtime and at compile-time are, we'll keep this function unstable until we've had that discussion.

Unresolved questions

RalfJung commented 4 years ago

I either forgot this exists or was never aware.^^ But it is certainly scary... pointer equality is a really complicated subject.

But also, so far the feature flag doesn't actually let you do anything, does it? binary_ptr_op still always errors in the CTFE machine, and even unleashed Miri cannot do pointer comparison.

oli-obk commented 4 years ago

Yea it's a fairly useless feature gate as of now

thomcc commented 2 years ago

should guaranteed_eq return an Option to allow the caller to know whether they got unclear results? What would the use-case for this be?

I came here to say yes. In general there's an ongoing issue with several APIs in const eval, where under const evaluation to assume there's no reason you might need to handle the case where the function fails to produce a meaningful result.

Some examples of this are align_offset returning usize::MAX, and align_to returning the whole slice in the first item. These are much worse to use (the first is a genuine footgun, and requires branching at runtime even in cases where the failure to align would be impossible except in const) because of this behavior, and IMO it's something we should stop doing.

RalfJung commented 2 years ago

FWIW, the align_offset contract is useful even for runtime code, since it enables more reliable detection of bad alignment code in sanitizers like Miri. But seeing all the confusion it caused, I agree this is not a great API.

align_to though I would consider less problematic. The "failure" case there is nicely covered by the types in the API I would say.

thomcc commented 2 years ago

align_to though I would consider less problematic. The "failure" case there is nicely covered by the types in the API I would say.

Sort of, but this could have been done in the appropriate cases as a.align_to::<T>().unwrap_or_else(|| (a, &[], &[]). As it is if you need to handle that, you can't. I don't think "yes, most people will use this formulation of unwrap_or_else to be a good argument for it doing it for you.

As it is, it not only means that you cannot rely on the alignment occurring for non-performance cases, but you also can't rely on it happening for performance cases either. It's pretty much set up only for loops that are exactly "handle head", "handle body", "handle tail", which is the rough shape, but you often want more flexibility than is allowed by that assumption.

This ends up being very frustrating when using the API, and I think its a pattern we shouldn't repeat.

RalfJung commented 2 years ago

It's pretty much set up only for loops that are exactly "handle head", "handle body", "handle tail", which is the rough shape, but you often want more flexibility than is allowed by that assumption.

That is explicitly and exclusively the case this API was designed for. So IMO it would have been a mistake to make the API worse for that case, and expand its scope. Rather, we should have other APIs for the other usecases. (And we can still add them, if someone has good ideas for useful APIs! I don't feel like Option<(&[T], &[U], &[T])> is terribly useful though, but who knows. Option<&[U]> sounds useful.)

Anyway, that's water under the bridge. guaranteed_eq is different, it's more like align_offset, so I can see the argument for returning an Option<bool>. OTOH the "guaranteed" in the name feels redundant then? It could in principle be just eq, and then guaranteed_eq is eq().unwrap_or(false) and guaranteed_ne is !eq().unwrap_or(true) (or eq().map(Neg::neg).unwrap_or(false)).

thomcc commented 2 years ago

That is explicitly and exclusively the case this API was designed for. So IMO it would have been a mistake to make the API worse for that case, and expand its scope.

FWIW, the things I'm referring to are things like being able to rely on there being less than size_of::<T>() items in some_u8s.align_to::<T>(), in the head. This kind of thing has been desirable every time I've used the function, and I've used it a lot. So I don't disagree this would meaningfully expand the scope.

An example from the stdlib of this is https://github.com/rust-lang/rust/blob/master/library/core/src/str/count.rs#L60-L69, which still exists at runtime, and causes worse codegen.

thomcc commented 2 years ago

OTOH the "guaranteed" in the name feels redundant then? It could in principle be just eq, and then guaranteed_eq is eq().unwrap_or(false) and guaranteed_ne is !eq().unwrap_or(true) (or eq().map(Neg::neg).unwrap_or(false)).

I agree on this point, although it makes it explicit that the Option is representing "no guarantee". It also has the benefit, of not shadowing PartialEq::eq.

thomcc commented 2 years ago

I tried to use this just now in some code wanting to assert that some struct definition was correct at compile time. (It failed, but I writing it that way was probably expecting too much of const eval, and I was able to rewrite it without this).

Anyway, initially my assert!(a.guaranteed_eq(b)) failed and when this happened I was left wondering if it was because my code sucked (and the struct definition was wrong), or because I had expected too much of const-eval (in which case I need to rework my assert, or give up).

It occurs to me though, that I only noticed this because my code had true be the success path -- If that was behind a false instead, it would been easy to miss. This further convinces me that guaranteed_{eq,ne} be exposed as an API that returns an Option.

While it might seem unlikely for someone to write code that does !a.guaranteed_eq(b) instead of a.guaranteed_ne(b), I could easily imagine it happening if the code had been ported from non-const code which had !a.eq(b).

Use of an option would force handling the "don't know at compile time" case in some way (ideally unwrap) and avoid a case where the API is similar enough to a non-const API that it becomes easy to do make a mistake when translating from one to the other.

RalfJung commented 2 years ago

We probably want to use a simpler type for the intrinsic (to make that easier to implement), but I am generally on-board with changing the type of the public functions here.