Operators and Expressions

Bitwise Operators

Bitwise operators manipulate the individual binary digits (bits) of integer values directly. Rust supports six bitwise operators on all signed and unsigned integer types (u8..u128, usize, i8..i128, isize): bitwise NOT (!), AND (&), OR (|), XOR (^), left shift (<<), and right shift (>>).

Overview of Bitwise Operators

Operator Name Syntax Bit-Level Behavior
! Bitwise NOT (complement) !x Flips every bit (0 becomes 1, 1 becomes 0)
& Bitwise AND a & b Result bit is 1 only if both corresponding bits are 1
| Bitwise OR (inclusive OR) a | b Result bit is 1 if at least one corresponding bit is 1
^ Bitwise XOR (exclusive OR) a ^ b Result bit is 1 if the corresponding bits are different
<< Left shift x << n Shifts bits left by n positions, filling low bits with 0
>> Right shift x >> n Shifts bits right by n positions (0-fill for unsigned; sign-fill for signed)

Note: Rust uses ! for bitwise NOT rather than C's tilde (~) operator. On bool, ! performs logical negation; on integers, ! inverts every bit.

Bit-by-Bit Truth Table

For each bit position $i$, &, |, ^, and ! follow this truth table:

Bit a Bit b !a a & b a | b a ^ b
0 0 1 0 0 0
0 1 1 0 1 1
1 0 0 0 1 1
1 1 0 1 1 0

Visualizing Bitwise Operations on u8

  a       = 0b1100_1010 (202)
  b       = 0b1010_1100 (172)
  ---------------------------
  !a      = 0b0011_0101 (53)    <-- every bit of `a` inverted
  a & b   = 0b1000_1000 (136)   <-- 1s only where both `a` and `b` have 1
  a | b   = 0b1110_1110 (238)   <-- 1s where either `a` or `b` has 1
  a ^ b   = 0b0110_0110 (102)   <-- 1s where `a` and `b` differ
fn main() {
    let a: u8 = 0b1100_1010;
    let b: u8 = 0b1010_1100;

    println!("a     = {:08b} ({a:3})", a);
    println!("b     = {:08b} ({b:3})", b);
    println!("!a    = {:08b} ({:3})", !a, !a);
    println!("a & b = {:08b} ({:3})", a & b, a & b);
    println!("a | b = {:08b} ({:3})", a | b, a | b);
    println!("a ^ b = {:08b} ({:3})", a ^ b, a ^ b);
}
a     = 11001010 (202)
b     = 10101100 (172)
!a    = 00110101 ( 53)
a & b = 10001000 (136)
a | b = 11101110 (238)
a ^ b = 01100110 (102)

Common Bitmask Idioms

Bitwise operators allow packing multiple boolean flags or compact fields into a single integer:

Task Idiom Explanation
Create mask for bit n 1 << n Places a 1 at bit position n (0-indexed from the least significant bit)
Set bit n to 1 flags |= 1 << n ORing with 1 sets bit n; ORing with 0 leaves other bits unchanged
Clear bit n to 0 flags &= !(1 << n) ANDing with 0 at bit n clears it; 1s elsewhere preserve other bits
Toggle (flip) bit n flags ^= 1 << n XORing with 1 inverts bit n; XORing with 0 preserves other bits
Test if bit n is 1 (flags & (1 << n)) != 0 Isolates bit n and checks whether the result is non-zero
const READ: u8 = 1 << 0;    // 0b0000_0001 (1)
const WRITE: u8 = 1 << 1;   // 0b0000_0010 (2)
const EXECUTE: u8 = 1 << 2; // 0b0000_0100 (4)

fn main() {
    // 1. Combine flags with bitwise OR (`|`)
    let mut perms: u8 = READ | WRITE;
    println!("Initial (R+W):   {:03b}", perms);

    // 2. Set EXECUTE bit
    perms |= EXECUTE;
    println!("Added EXECUTE:   {:03b}", perms);

    // 3. Clear WRITE bit
    perms &= !WRITE;
    println!("Cleared WRITE:   {:03b}", perms);

    // 4. Toggle READ bit
    perms ^= READ;
    println!("Toggled READ:    {:03b}", perms);

    // 5. Test if EXECUTE is set
    let can_exec = (perms & EXECUTE) != 0;
    println!("Can execute?     {can_exec}");
}
Initial (R+W):   011
Added EXECUTE:   111
Cleared WRITE:   101
Toggled READ:    100
Can execute?     true

Left Shift (<<) and Right Shift (>>)

  • Left shift (x << n): Moves all bits n positions toward the most significant bit and fills the vacated low-order bits with 0. Shifting left by n without overflow multiplies x by $2^n$.
  • Right shift (x >> n): Moves all bits n positions toward the least significant bit. What fills the vacated high-order bits depends on whether x is unsigned or signed:
    • Unsigned types (u8..usize): Logical right shift—fills vacated left bits with 0.
    • Signed types (i8..isize): Arithmetic right shift—fills vacated left bits with copies of the sign bit (1 if negative, 0 if non-negative).
Logical Right Shift on u8 (0b1001_0100 >> 2):
  0 0 -> [1 0 0 1 0 1 | 0 0] (dropped)   ==>   0b0010_0101 (37)

Arithmetic Right Shift on i8 (-108 == 0b1001_0100 >> 2):
  1 1 -> [1 0 0 1 0 1 | 0 0] (dropped)   ==>   0b1110_0101 (-27)
fn main() {
    let u: u8 = 0b1001_0100; // 148
    let s: i8 = -108;        // 0b1001_0100 in two's complement

    println!("u << 1 = {:08b} ({})", u << 1, u << 1);
    println!("u >> 2 = {:08b} ({})", u >> 2, u >> 2);
    println!("s >> 2 = {:08b} ({})", (s >> 2) as u8, s >> 2);
}
u << 1 = 00101000 (40)
u >> 2 = 00100101 (37)
s >> 2 = 11100101 (-27)

Shift Count Types and Shift Overflow

Unlike &, |, and ^—which require both operands to have the exact same integer type—the right-hand operand (shift distance) of << and >> can be any integer type (for example, 1_u64 << 3_u8 is valid).

However, shifting by a negative number or by a number of bits greater than or equal to the bit width of the left operand (such as x << 8 on a u8 or x << 32 on a u32) is a shift overflow:

// ❌ does not compile
fn main() {
    let x: u8 = 1;
    let shifted = x << 8;
    println!("{shifted}");
}
error: this arithmetic operation will overflow
 --> src/main.rs:3:19
  |
3 |     let shifted = x << 8;
  |                   ^^^^^^ attempt to shift left by `8_i32`, which would overflow
  |
  = note: `#[deny(arithmetic_overflow)]` on by default

error: aborting due to 1 previous error

When the shift distance is computed dynamically at runtime and may exceed the bit width, use .checked_shl(), .wrapping_shl(), or .overflowing_shl().

Built-In Bit Manipulation Methods

Every integer type in Rust provides intrinsic methods that compile directly to single CPU instructions (such as POPCNT, LZCNT, TZCNT, and ROR/ROL):

Method Example on 0b0001_1010_u8 Result Description
.count_ones() x.count_ones() 3 Number of 1 bits (population count / Hamming weight)
.count_zeros() x.count_zeros() 5 Number of 0 bits
.leading_zeros() x.leading_zeros() 3 Consecutive 0 bits starting from the most significant bit
.trailing_zeros() x.trailing_zeros() 1 Consecutive 0 bits starting from the least significant bit
.rotate_left(n) x.rotate_left(2) 0b0110_1000 Circular bit shift left (bits shifted off the top wrap to the bottom)
.rotate_right(n) x.rotate_right(2) 0b1000_0110 Circular bit shift right
.reverse_bits() x.reverse_bits() 0b0101_1000 Reverses the order of all bits
.swap_bytes() 0x1234_u16.swap_bytes() 0x3412_u16 Reverses byte order (endianness conversion)
fn main() {
    let mask: u8 = 0b0001_1010;

    println!("ones:           {}", mask.count_ones());
    println!("leading_zeros:  {}", mask.leading_zeros());
    println!("trailing_zeros: {}", mask.trailing_zeros());
    println!("rotate_left(4): {:08b}", mask.rotate_left(4));
    println!("reverse_bits:   {:08b}", mask.reverse_bits());
}
ones:           3
leading_zeros:  3
trailing_zeros: 1
rotate_left(4): 10100001
reverse_bits:   01011000

Precedence of Bitwise Operators

Within an expression, bitwise operators follow this precedence order from highest to lowest:

  1. ! (Bitwise NOT)
  2. +, - (Arithmetic addition and subtraction)
  3. <<, >> (Bit shifts)
  4. & (Bitwise AND)
  5. ^ (Bitwise XOR)
  6. | (Bitwise OR)
  7. ==, !=, <, >, <=, >= (Comparisons)

Unlike C—where == and != bind tighter than &, ^, and |—Rust places all bitwise operators above comparison operators, so flags & mask != 0 evaluates (flags & mask) first. However, because arithmetic + and - bind tighter than << and & (meaning 1 << 2 + 1 is parsed as 1 << (2 + 1)), using explicit parentheses around bit shifts and masks (flags & mask) != 0 is standard practice.