Discussion: Const requirements

I was looking at the documentation for AtomicU8::compare_exchange and noticed that there was a very specific set of instructions on what is allowed in terms of the Ordering parameters.

I would imagine that the use of this would be made better if such things were checked at compile time. This is also not the only place that such compile checks would be nice to have.

Therefore I ask: is there any desire to have such checks be, at least in part, possible?

What I am imagining are some sort of annotations, sort of like where clauses that are attempted at compile time. To facilitate this, we could also make calling functions with these checks unsafe if the compiler cannot guarantee that it checks out.

Example: (Assuming std::cmp::Ord is implemented for Ordering)

pub fn compare_exchange(
    &self,
    current: u8,
    new: u8,
    success: Ordering,
    failure: Ordering
) -> Result<u8, u8> 
where 
    success >= failure,
    failure == Ordering::SeqCst
        || failure == Ordering::Acquire
        || failure == Ordering::Relaxed
2 Likes

While this is a very good idea (comparable to C++ contracts), I think atomics are the wrong use-case. Adding this sort of compile-time checking to atomics is dangerous because they are still subtle to use and it will lull people into shooting their feet off.

I have had this idea for some time, but it was when I was reading the atomic's documentation that I finally came up with a public example of what could be made better.

However, I don't quite see how this specific check would lull people into a dangerous situation anymore than the current panics do.

I also don't understand why these extra checks would give any particular false sense of security, but there's a more serious show-stopper for applying this kind of compile-time checking to existing APIs: it's backwards incompatible. Unless you make uses with runtime values only warn instead of error, but at that point it's not clear to me why we should add a new language feature instead of a lint.

1 Like

So C++ contracts basically panic if a pre-/post-condition fails (runtime checks) and they can be turned off completely (in rust it would probably be debug_assert!(...).

I don't quite see how this is backwards incompatible in general (specific cases may vary). For the function call that I initially linked the values that I check are panic!(...) in the original, so no code would have those values anyway.

One kind of backwards incompatibility is if the function which is changed to require constant parameters (or else unsafe, as you proposed as escape hatch) was wrapped and the wrapper uses runtime parameters (because the const parameter equivalent didn't exist when it was written), for example:

fn my_cmpxchg(x: &AtomicU8, success: Ordering, failure: Ordering) -> Result<u8, u8> {
    x.compare_exchange(0, 1, success, failure)
}
4 Likes

Ah makes sense. However, I think that how we roll such a feature out should be discussed later. I used this as an example of why it might be useful

I don't think there's a need for anything compile-time here -- either making a more specific type or already-on-the-list constraints on const generics will be sufficient.

I think that this could be re-phrased into a runtime thing that seems interesting to me, though. We have lots of methods that start by assert!ing something, and making that more visible to callers seems interesting.

For example, .step_by(0) panics, and (iirc) there's a special-case clippy lint to help catch that. What if, instead of being hidden in the implementation, one could do #[assert(step > 0)] on the method? Then built-in lints could try to check those things, while still doing the check at runtime. That way it could be automatically-documented too.

(And for compare_exchange, maybe it'd be a #[debug_assert], which lints could still use even if nothing happens at runtime.)

1 Like

I think adding debug_asserts to those functions would be great, but right now we can't do it properly, see discussion in this issue.

And I would also love for Rust to move a bit closer to SPARK in terms of powerful compile-time contracts.

1 Like

The ordering parameters really should be const generic parameters, like

pub fn compare_exchange<
    const SUCCESS: Ordering,
    const FAILURE: Ordering
>(
    &self,
    current: u8,
    new: u8
) -> Result<u8, u8> 

After all, the assembly code is different for each possible ordering. In fact, the implementations of these functions contain a match over all possible orderings, which the optimizer will fold away if the ordering is constant, but which generates quite a lot of code if it isn't.

But const generics didn't exist when these APIs were created.

For const generic parameters, I agree that booleans in where clauses would be very useful. But doing it for runtime parameters would be a huge can of worms.

5 Likes

Small nitpick: const arguments is a separate feature (which I would really like to see implemented one day) and is not entailed by const generics.

Yes it would be nice that asserts and debug asserts placed in a specific way to be part of the documentation but I see that as a different concept and less useful since it isn't done at compile time.

One of the great benefits I see in Rust is compile time guarantees. And there are definite ergonomic arguments for not always making new types. Especially when there needs to some relation between two different parameters.

To negate some of the annoyance of unsafe functions we could treat debug and normal asserts as verifying the preconditions even if the runtime check doesn't happen in release mode.

Sure. But those are best done on compile-time parameters, not runtime ones. And @rkruppe already pointed out that there cannot be compile-time checks on the existing functions, so the new ones with the compile-time checks can just take compile-time parameters (as in @comex's post).

Then it's just value-constraints on const generics, which have been on the wish-list for a while, but are hard enough then even simple cases like 1+1=2 were left out of RFC 2000.

Nitpick to your nitpick: Const generics as currently implemented would make possible the modified API from my last post, where the ordering parameters are moved within angle brackets. Const arguments would make possible an API that keeps the parameters in their current position while requiring them to be constants.