Operators and Expressions

Logical Operators

Logical operators combine or invert boolean (bool) expressions and always produce a bool result (true or false). Rust provides three logical operators: logical NOT (!), logical AND (&&), and logical OR (||).

Overview of Logical Operators

Operator Name Arity Syntax Evaluates to true When
! Logical NOT Unary (prefix) !a a is false
&& Logical AND Binary (infix) a && b Both a and b are true
|| Logical OR Binary (infix) a || b At least one of a or b is true
fn main() {
    let is_authenticated = true;
    let is_admin = false;

    println!("!is_authenticated:            {}", !is_authenticated);
    println!("is_authenticated && is_admin: {}", is_authenticated && is_admin);
    println!("is_authenticated || is_admin: {}", is_authenticated || is_admin);
}
!is_authenticated:            false
is_authenticated && is_admin: false
is_authenticated || is_admin: true

Truth Tables

Every combination of bool inputs for unary ! and binary && and || is defined by the following truth tables:

Unary Logical NOT (!)

a !a
true false
false true

Binary Logical AND (&&) and Logical OR (||)

a b a && b a || b
true true true true
true false false true
false true false true
false false false false

Strict bool Operand Requirement

Unlike C, C++, Python, or JavaScript, Rust does not implicitly convert integers, strings, slices, or pointers into booleans. There is no concept of "truthy" or "falsy" values. Both operands of && and || must have the exact type bool.

Passing an integer or string to && or || is a compile-time type error:

// ❌ does not compile
fn main() {
    let count = 5;
    let ready = true;

    if count && ready {
        println!("Running");
    }
}
error[E0308]: mismatched types
 --> src/main.rs:5:8
  |
5 |     if count && ready {
  |        ^^^^^ expected `bool`, found integer

error: aborting due to 1 previous error

Write an explicit comparison such as count != 0 && ready or !name.is_empty() to produce a bool.

The ! Operator on bool vs Integers

While && and || are defined exclusively for bool, the ! operator is overloaded in the standard library (std::ops::Not) for both bool and integer types:

  • On bool, !x performs logical negation (!true == false).
  • On integers (u8, i32, usize, etc.), !x performs bitwise NOT (inverting every bit, equivalent to ~x in C).

Because ! is valid on integers and returns an integer rather than a bool, writing if !count (a common C idiom for testing whether count == 0) fails to compile in Rust:

// ❌ does not compile
fn main() {
    let count: u8 = 0;

    // `!count` flips all 8 bits to `255_u8`, which is NOT a `bool`!
    if !count {
        println!("count is zero");
    }
}
error[E0308]: mismatched types
 --> src/main.rs:5:8
  |
5 |     if !count {
  |        ^^^^^^ expected `bool`, found `u8`

error: aborting due to 1 previous error

Always write count == 0 instead of !count when testing whether a number is zero.

Operator Precedence and Grouping

When an expression combines comparisons and multiple logical operators without parentheses, Rust evaluates them according to a strict precedence hierarchy from highest to lowest:

Highest Precedence
  1.  !                  (Logical NOT - unary prefix)
  2.  ==  !=  <  >  <=  >= (Comparison operators)
  3.  &&                 (Logical AND)
  4.  ||                 (Logical OR)
Lowest Precedence

Because comparison operators bind tighter than && and ||, expressions like x > 0 && x < 10 are parsed automatically as (x > 0) && (x < 10). Likewise, because && binds tighter than ||, a || b && c is parsed as a || (b && c):

fn main() {
    let a = true;
    let b = false;
    let c = false;

    // Parsed as `a || (b && c)` -> `true || false` -> `true`
    let default_precedence = a || b && c;

    // Explicit parentheses override precedence: `(a || b) && c` -> `true && false` -> `false`
    let grouped = (a || b) && c;

    println!("a || b && c   = {default_precedence}");
    println!("(a || b) && c = {grouped}");
}
a || b && c   = true
(a || b) && c = false

Precedence of ! with Comparisons

Because unary ! has higher precedence than ==, <, and >, writing !x == y is parsed as (!x) == y. If x and y are integers, (!x) performs a bitwise NOT on x before comparing it to y! To negate the result of a comparison, either use != or wrap the comparison in parentheses !(x < y):

fn main() {
    let x = 10;
    let y = 20;

    let not_less = !(x < y);
    println!("!(10 < 20) = {not_less}");
}
!(10 < 20) = false

Chaining Range Checks

In mathematics and Python, 0 < x < 10 checks whether x lies between 0 and 10. In Rust, comparison operators cannot be chained directly; both conditions must be joined with && (or expressed with .contains(&x) on a range):

// ❌ does not compile
fn main() {
    let x = 5;
    if 0 < x < 10 {
        println!("in range");
    }
}
error: comparison operators cannot be chained
 --> src/main.rs:3:10
  |
3 |     if 0 < x < 10 {
  |          ^   ^
  |
help: split the comparison into two
  |
3 |     if 0 < x && x < 10 {
  |              ++++++++

error: aborting due to 1 previous error

Joining the two comparisons with && compiles and runs as intended:

fn main() {
    let x = 5;
    if 0 < x && x < 10 {
        println!("{x} is strictly between 0 and 10");
    }
}
5 is strictly between 0 and 10

De Morgan's Laws

Negating a compound boolean expression follows De Morgan's laws, which distribute ! across && and || by flipping the operator:

  • !(a && b) is logically equivalent to !a || !b
  • !(a || b) is logically equivalent to !a && !b
fn main() {
    for a in [false, true] {
        for b in [false, true] {
            assert_eq!(!(a && b), !a || !b);
            assert_eq!(!(a || b), !a && !b);
        }
    }
    println!("De Morgan's laws verified for all boolean inputs.");
}
De Morgan's laws verified for all boolean inputs.

Eager (Non-Short-Circuiting) Boolean Operators and XOR

The && and || operators are lazy (short-circuiting): a && b skips evaluating b if a is false, and a || b skips evaluating b if a is true.

When you want both boolean operands to be evaluated unconditionally—or when you need an exclusive OR (XOR) operation—Rust also supports the single-character operators &, |, and ^ (or !=) on bool values:

Expression Behavior on bool Operands Evaluates Right Operand Always?
a && b Short-circuiting logical AND No (skipped if a == false)
a || b Short-circuiting logical OR No (skipped if a == true)
a & b Eager logical AND Yes
a | b Eager logical OR Yes
a ^ b or a != b Logical XOR (true when a and b differ) Yes
fn step(name: &str, result: bool) -> bool {
    println!("  evaluated {name} -> {result}");
    result
}

fn main() {
    println!("Short-circuit &&:");
    let r1 = step("left", false) && step("right", true);
    println!("Result: {r1}\n");

    println!("Eager &:");
    let r2 = step("left", false) & step("right", true);
    println!("Result: {r2}\n");

    println!("Logical XOR (^):");
    println!("true ^ false = {}", true ^ false);
    println!("true ^ true  = {}", true ^ true);
}
Short-circuit &&:
  evaluated left -> false
Result: false

Eager &:
  evaluated left -> false
  evaluated right -> true
Result: false

Logical XOR (^):
true ^ false = true
true ^ true  = false