Ruta graveolens  ·  notes from a language experiment  ·  cultivated since 2025

Bitwise Operators

Bitwise operators perform bit-level operations on integer values.

Binary Bitwise Operators

Binary bitwise operators take two operands of the same integer type and produce a value of that same type. Each operator acts on the two operands' w-bit two's-complement representations bit-by-bit and the result is that bit pattern reinterpreted at the operand type's signedness; unlike arithmetic, a bitwise operation never traps (core calculus docs/formal/01-core-calculus.md §6.4, rule (D-Bit)).

OperatorNameDescription
&Bitwise ANDSets each bit if both operand bits are 1
|Bitwise ORSets each bit if either operand bit is 1
^Bitwise XORSets each bit if exactly one operand bit is 1
fn main() -> i32 {
    let a: i32 = 0b1100;
    let b: i32 = 0b1010;
    let and = a & b;   // 0b1000 = 8
    let or = a | b;    // 0b1110 = 14
    let xor = a ^ b;   // 0b0110 = 6
    and
}

Bitwise NOT

The bitwise NOT operator ~ inverts all bits of its operand.

Bitwise NOT takes a single integer operand and produces a value of the same type whose bits are the complement of the operand's w-bit two's-complement representation, reinterpreted at that signedness (core calculus docs/formal/01-core-calculus.md §6.4, the ~ case of rule (D-Bit)).

fn main() -> i32 {
    let a: i32 = 0b0101;
    let not_a = ~a;   // All bits inverted
    0
}

Shift Operators

Shift operators move bits left or right by a specified number of positions.

OperatorNameDescription
<<Left ShiftShifts bits left, filling with zeros
>>Right ShiftShifts bits right

For left shift (<<), vacated bit positions are filled with zeros.

For right shift (>>), the behavior depends on the signedness of the operand type:

  • For unsigned types, vacated bit positions are filled with zeros (logical shift).
  • For signed types, vacated bit positions are filled with copies of the sign bit (arithmetic shift).

(Core calculus docs/formal/01-core-calculus.md §6.4, rule (D-Shr): sign-replicating on a signed operand type, zero-filling on an unsigned one.)

The shift amount operand MUST have the same type as the value being shifted.

If the shift amount is greater than or equal to the bit width of the type, the behavior is defined as shifting by the amount modulo the bit width. For example, shifting an i32 by 33 positions is equivalent to shifting by 1 position. (Core calculus docs/formal/01-core-calculus.md §6.4, rules (D-Shl)/(D-Shr): the shift amount is reduced modulo the operand width w before shifting. Shifting never traps.)

fn main() -> i32 {
    let x: i32 = 1;
    let left = x << 4;    // 16 (binary: 10000)
    let right = left >> 2; // 4  (binary: 100)
    right
}

Operator Precedence

Bitwise operator precedence (highest to lowest within this group):

  1. ~ (bitwise NOT, unary)
  2. <<, >> (shift operators)
  3. & (bitwise AND)
  4. ^ (bitwise XOR)
  5. | (bitwise OR)

Rue uses the same operator precedence as Rust. Shift operators have lower precedence than the arithmetic operators, and the bitwise AND, XOR, and OR operators have higher precedence than the comparison operators. The complete binary operator precedence, from highest (binds tightest) to lowest:

  1. Unary -, !, ~
  2. *, /, %
  3. +, -
  4. <<, >>
  5. &
  6. ^
  7. |
  8. ==, !=, <, >, <=, >=
  9. &&
  10. ||

For example, a << b + c parses as a << (b + c), a << b * c parses as a << (b * c), and a & b == c parses as (a & b) == c.

Parentheses can be used to override the default precedence.

fn main() -> i32 {
    let a: i32 = 1 | 2 & 3;   // = 1 | (2 & 3) = 1 | 2 = 3
    let b: i32 = (1 | 2) & 3; // = 3 & 3 = 3
    a
}

Associativity

All binary bitwise operators are left-associative.

fn main() -> i32 {
    let x: i32 = 1 << 2 << 1;  // = (1 << 2) << 1 = 4 << 1 = 8
    x
}

Type Checking

Bitwise operators are only valid for integer types (i8, i16, i32, i64, u8, u16, u32, u64).

Using bitwise operators on boolean or other non-integer types is a compile-time error.