Let Statements

A let statement introduces a new variable binding.

let_stmt = "let" [ "mut" ] IDENT [ ":" type ] "=" expression ";" ;

Immutable Bindings

By default, variables are immutable. An immutable variable MUST NOT be reassigned.

fn main() -> i32 {
    let x = 42;
    x
}

Mutable Bindings

The mut keyword creates a mutable binding that MAY be reassigned.

fn main() -> i32 {
    let mut x = 10;
    x = 20;
    x
}

Type Annotations

Type annotations are optional when the type can be inferred from the initializer.

When a type annotation is present, the initializer MUST be compatible with that type.

fn main() -> i32 {
    let x: i32 = 42;      // explicit type
    let y = 10;           // type inferred as i32
    let z: i64 = 100;     // 100 inferred as i64
    x + y
}

Shadowing

A variable MAY shadow a previous variable of the same name in the same scope.

When shadowing, the new variable MAY have a different type.

The scope of a binding introduced by a let statement begins after the complete let statement, including its initializer. The initializer expression is evaluated before the new binding is introduced, so references to a shadowed name within the initializer resolve to the previous binding.

fn main() -> i32 {
    let x = 10;
    let x = x + 5;  // shadows previous x, initializer uses old x
    x  // 15
}

Struct Destructuring

A let statement may destructure a struct value, binding each field to a new variable. The struct type name must be specified, and all fields must be listed.

let_destructure = "let" type_name "{" field_bindings "}" "=" expression ";" ;
field_bindings  = field_binding { "," field_binding } [ "," ] ;
field_binding   = [ "mut" ] IDENT [ ":" ( IDENT | "_" ) ] ;

The expression must evaluate to the named struct type. The struct value is consumed by the destructuring — it is no longer accessible after destructuring.

All fields of the struct MUST be listed in the destructuring pattern. Omitting a field is a compile-time error.

A field binding of the form field (shorthand) binds the field value to a new variable with the same name. A binding of the form field: name binds the field value to a variable named name. A binding of the form field: _ discards the field value, dropping it immediately if the type has a destructor.

struct Point { x: i32, y: i32 }

fn main() -> i32 {
    let p = Point { x: 10, y: 32 };
    let Point { x, y } = p;    // p is consumed
    x + y                       // 42
}
struct Pair { a: i32, b: i32 }

fn main() -> i32 {
    let p = Pair { a: 1, b: 2 };
    let Pair { a: first, b: _ } = p;   // rename a to first, discard b
    first                                // 1
}

Tuple Destructuring

A tuple destructuring pattern has the form (b0, b1, ..., bN-1), where each bi is either an identifier binding (optionally preceded by mut) or the wildcard _. A 1-tuple pattern MUST have a trailing comma: (b,).

The initializer expression MUST evaluate to a tuple type. The arity of the pattern MUST equal the arity of the tuple type; mismatched arities are a compile-time error.

An identifier binding x or mut x introduces a new local x of the element's type, initialised from the element at the corresponding position. The mut keyword makes the binding mutable.

A wildcard binding _ discards the element at that position, dropping it immediately if the type has a destructor. The wildcard does not introduce a local.

The tuple value is consumed by destructuring. After a let (b0, ..., bN-1) = t; statement, t is no longer accessible.

fn main() -> i32 {
    let t = (10, 20, 30);
    let (a, _, c) = t;        // discards the middle element
    a + c                      // 40
}
fn main() -> i32 {
    let (mut x, y) = (1, 41);
    x = x + y;
    x                          // 42
}