Operators and Expressions

Operator Precedence

Operator precedence determines how an expression containing multiple operators is grouped when parentheses are omitted, and associativity determines the direction in which operators of the same precedence level are grouped (left-to-right, right-to-left, or non-associative).

Complete Rust Operator Precedence Table

The following table lists all Rust operators from highest precedence (1, binds tightest) to lowest precedence (15, binds loosest):

Level Category Operators Associativity Example (a op b op c)
1 Paths, method calls, field access, indexing, call, ? :: . () [] ? Left-to-right obj.items[0].parse()?
2 Unary prefix operators - ! * & &mut Right-to-left (prefix) &*ptr -> &(*ptr)
3 Type cast as Left-to-right x as i64 as f64
4 Multiplicative arithmetic * / % Left-to-right a / b * c -> (a / b) * c
5 Additive arithmetic + - Left-to-right a - b + c -> (a - b) + c
6 Bitwise shifts << >> Left-to-right x << 2 >> 1 -> (x << 2) >> 1
7 Bitwise AND & Left-to-right a & b & c -> (a & b) & c
8 Bitwise XOR ^ Left-to-right a ^ b ^ c -> (a ^ b) ^ c
9 Bitwise OR | Left-to-right a | b | c -> (a | b) | c
10 Relational comparisons == != < > <= >= None (cannot chain!) a == b
11 Logical AND && Left-to-right a && b && c -> (a && b) && c
12 Logical OR || Left-to-right a || b || c -> (a || b) || c
13 Ranges .. ..= None (requires parens) 0..10
14 Assignment & compound assignment = += -= *= /= %= &= |= ^= <<= >>= Right-to-left x += y
15 Control-flow jumps return break yeet Right-to-left (prefix) return a + b -> return (a + b)

Precedence vs Order of Evaluation

It is critical to distinguish precedence (how sub-expressions are grouped into a tree) from order of evaluation (the chronological sequence in which sub-expressions execute at runtime):

  • Precedence decides that f() + g() * h() is grouped as f() + (g() * h()).
  • Order of evaluation in Rust is left-to-right for binary operators: even though g() * h() has higher precedence than +, Rust still calls f() first, then g(), then h(), and finally multiplies the results of g() and h() before adding to f()!
fn step(label: &str, value: i32) -> i32 {
    println!("  evaluated {label} -> {value}");
    value
}

fn main() {
    println!("Computing step(\"A\", 2) + step(\"B\", 3) * step(\"C\", 4):");
    let result = step("A", 2) + step("B", 3) * step("C", 4);
    println!("Result: {result}");
}
Computing step("A", 2) + step("B", 3) * step("C", 4):
  evaluated A -> 2
  evaluated B -> 3
  evaluated C -> 4
Result: 14

Important Precedence Interactions and Pitfalls

1. Method Calls (.) Bind Tighter Than Unary -, !, *, and as

Because . is at Level 1 (the very top of the table) while unary - and as are at Levels 2 and 3, method calls always execute before unary negation, dereferencing, or as casts:

Written Expression How Rust Parses It Usually Intended Expression
-4_i32.pow(2) -(4_i32.pow(2)) -> -16 (-4_i32).pow(2) -> +16
*ptr.field *(ptr.field) (*ptr).field (though . auto-dereferences ptr anyway)
!flag.is_valid() !(flag.is_valid()) Matches expectation!
x as f64.sqrt() Syntax error (f64.sqrt() invalid type) (x as f64).sqrt()
(0..5).rev() Calls .rev() on 0..5 0..5.rev() would call .rev() on 5!
fn main() {
    // `.pow(2)` runs on `4_i32` FIRST, and unary `-` negates the result `16` -> `-16`!
    let unparenthesized = -4_i32.pow(2);

    // Parentheses force `-4_i32` to be squared: `(-4) * (-4)` -> `+16`
    let parenthesized = (-4_i32).pow(2);

    println!("-4_i32.pow(2)   = {unparenthesized}");
    println!("(-4_i32).pow(2) = {parenthesized}");
}
-4_i32.pow(2)   = -16
(-4_i32).pow(2) = 16

2. Type Cast (as) Binds Tighter Than Arithmetic (+, -, *, /)

Because as (Level 3) has higher precedence than *, /, +, and - (Levels 4 and 5), writing a + b as f64 casts only b to f64, not a + b:

fn main() {
    let a: i32 = 3;
    let b: i32 = 2;

    // `(a / b) as f64` performs integer division `3 / 2 == 1` FIRST, then casts `1` to `1.0`
    let int_div_then_cast = (a / b) as f64;

    // `a as f64 / b as f64` casts both operands first (`3.0 / 2.0`), producing `1.5`
    let float_div = a as f64 / b as f64;

    println!("(a / b) as f64       = {int_div_then_cast:.1}");
    println!("a as f64 / b as f64  = {float_div:.1}");
}
(a / b) as f64       = 1.0
a as f64 / b as f64  = 1.5

3. Additive (+, -) Binds Tighter Than Bitwise Shifts (<<, >>)

In both C and Rust, + and - (Level 5) bind tighter than << and >> (Level 6). Therefore, 1 << 2 + 1 is parsed as 1 << (2 + 1) (8), not (1 << 2) + 1 (5):

fn main() {
    let ungrouped = 1 << 2 + 1;   // Parsed as 1 << (2 + 1) = 8
    let grouped = (1 << 2) + 1;   // 4 + 1 = 5

    println!("1 << 2 + 1   = {ungrouped}");
    println!("(1 << 2) + 1 = {grouped}");
}
1 << 2 + 1   = 8
(1 << 2) + 1 = 5

4. Bitwise Operators (&, ^, |) Bind Tighter Than Comparisons (==, !=)

Fixing a classic design flaw in C, Rust gives &, ^, and | (Levels 7–9) higher precedence than == and != (Level 10). Thus flags & MASK == MASK is parsed as (flags & MASK) == MASK rather than flags & (MASK == MASK).

Left-to-Right Associativity of -, /, and %

When operators of the exact same precedence level appear in a row, left-to-right associativity means they group from the left:

  • 20 - 5 - 3 is (20 - 5) - 3 = 12 (not 20 - (5 - 3) = 18).
  • 24 / 6 / 2 is (24 / 6) / 2 = 2 (not 24 / (6 / 2) = 8).
fn main() {
    println!("20 - 5 - 3 = {}", 20 - 5 - 3);
    println!("24 / 6 / 2 = {}", 24 / 6 / 2);
}
20 - 5 - 3 = 12
24 / 6 / 2 = 2