Why does Rust use the same symbol (!) for bitwise NOT or inverse and logical negation?

I thought Rust is a strongly typed language so the compiler would outright reject the code that uses this ! symbol for other than logically negating a boolean value.

Took me a while to figure out this which caused a bug in my code(Because I thought ! only negated the boolean data type, and the compiler won't compile if use this symbol for other contexts).

Instead of writing !(AGE.fetch_sub(1, Acquire) > 0) I wrote
!AGE.fetch_sub(1, Acquire) > 0. And it caused the bug. I expected the compiler will complain as I was applying the ! symbol to negate an integer type, but it did not.

1 Like

Correct.

Incorrect, since this operator is tied to the Not operation trait, which can be implemented on user types and, as you've already noticed, is implemented for numbers.

5 Likes

The ! operator for integers is defined as the bitwise NOT; ie. what's in most C-like languages spelled ~. This is possible precisely because Rust is strongly typed. Presumably Rust doesn’t have separate operators because it’s not strictly necessary (and it’s the same operation anyway if you think of booleans as 1-bit integers).

6 Likes

Non sequitur. Like, literally – the false consequence you are drawing does not, in any way, follow from the antecedent.


Also note that whatever you expect, the "logical" and "bitwise" negation of a boolean is, and will always, be one and the same operation. A boolean is homomorphic with a 1-bit integer.

3 Likes

I don't do a lot of bit manipulation, but would this be a good candidate for a clippy lint? !2 > 0 feels doesn't seem like something that you'd be intending to do even if you were deliberately wanting to NOT the int.

Clippy does warn on !(2 > 0) though - suggesting 2 <= 0. (I personally find easier to read but that's not really the point here)

2 Likes

How about the performance of <=?

What do you mean?

!(a > b) and a <= b seem to generate the same instructions.

Rust code
#![no_std]

#[inline(never)]
#[no_mangle]
pub fn one(a: i32, b: i32) -> bool {
    !(a > b)
}

#[inline(never)]
#[no_mangle]
pub fn two(a: i32, b: i32) -> bool {
    a <= b
}
armv7r-none-eabihf
one:
        mov     r2, #0
        cmp     r0, r1
        movwle  r2, #1
        mov     r0, r2
        bx      lr

.set two, one
aarch64-unknown-linux-gnu
one:
        cmp     w0, w1
        cset    w0, le
        ret

.set two, one
x86_64-unknown-linux-gnu
one:
        cmp     edi, esi
        setle   al
        ret

.set two, one
1 Like

To add a little bit of context, C has separate operators for logical negation (!) and bitwise negation (~) in significant part because before C99, C didn't have a boolean type, and instead if works by comparing the controlling expression against literal 0. So C needs a separate logical negation that maps any nonzero value to zero. This is also why you'll sometimes see pre-C99 projects do #define TRUE (!FALSE) instead of using 1.

Rust uses strongly typed semantic dispatch with a logically single-bit bool, thus making “logical” negation the same thing as bitwise negation.

For primitive types, it is fundamentally equivalent. And LLVM is very good at doing this kind of logical equivalence optimization, so long as your type isn't doing something silly like violating De Morgan's laws. (Like, say, f32 does.)

Comparison is implemented in terms of three-way comparison. !(a > b) is !(matches!(cmp(&a, &b), Greater)) and a <= b is matches!(cmp(&a, &b), Less | Equal), which are trivially semantically identical to LLVM's optimizations.

I agree such a lint would be reasonable, especially since (!a) > b doesn't lint against unused parens.

I was going to note that this is actually a different operation for PartialOrd types like f32, but that's actually a different lint from the one for Ord types.

(Since I'm making a post anyway) Generally I've found the most readable option to be to almost always use < or <=, writing the compared values in expected-increasing order. If evaluation order of the temporaries matters, hoist them to named variables.

I only use > or >= for comparing a variable value to a constant (or at least invariable) value, but even then I'm replacing if idx >= len with if len <= idx more and more lately. It's just if 0 < var that still feels like a weird “Yoda conditional” to me (unless it also has && var < cap).

4 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.