Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Binding and assignment

name = expression first declares an immutable local with inferred type. name: Type = expression adds an annotation. mut name = expression or mut name: Type = expression declares a mutable local.

The value of a binding or a single-target assignment is an expression, an if block, or a match block. The selected suite supplies a block’s value.

Later name = value assigns to an existing local and requires mut and the same type. Repeating mut or an annotation for that name is a duplicate declaration. Parameters are immutable, and declarations always have an initializer. Copies, moves, and exclusive access follow ownership.

Field and indexed assignment accept nested writable paths such as rows[0][-1] = 9 and records[0].values[1] = 7. The root must be mutable or an exclusive reference. Existing list slots change in place, without copying or allocating their containing owners. Dictionaries have no indexed assignment, so an assignment path cannot write through dictionary[key]; use insert(key, value)? to add or replace, or get_mut to change a value in place.

Assignment proceeds in this order:

  1. Evaluate the right-hand value.
  2. Evaluate destination indices once each, from root to leaf.
  3. Resolve the destination using the resulting contents, checking bounds or keys.
  4. Replace the destination while holding exclusive access.

Index expressions can read or resize the destination. Their values are saved before any address is resolved, and no user expression runs between address resolution and the write. Replacing an owned value destroys its previous value exactly once. If an index propagates an error, a pending right-hand owner is cleaned up and no write occurs.

Whole-instance assignment through *reference = instance is rejected for class references, including class-valued field references. Replace the owning binding or a named field instead.

Augmented assignment

target += value, -=, *=, /=, //=, %= and integer bitwise updates &=, |=, ^=, <<=, >>= update an existing numeric destination. The destination can be a mutable binding, field, collection element, or exclusive reference, including nested paths. The right side takes the destination’s numeric type. Integer division uses //=, float division uses /=, and %= currently accepts integers. Strings and collections do not support augmented assignment; string concatenation remains fallible and explicit. Inside a checked: block, an integer update that overflows or divides by zero exits the block with Err before writing back, so the destination keeps its previous value.

Unlike ordinary assignment, augmented assignment evaluates its target before the right side:

  1. Evaluate each target index once, from root to leaf, saving its value.
  2. Read the destination’s current scalar value.
  3. Evaluate the right side and apply the arithmetic operation.
  4. Write back to the same destination using the saved indices.

A shared loan protects the destination during right-side evaluation. The right side may observe it (count += count), but cannot move, replace, or resize its owner. Collection loans cover the whole collection unless distinct literal list indices separate them. Index expressions may mutate the owner before the destination is read. If an index or the right side propagates an error, the update performs no write. Invalid list indices trap as in ordinary indexed assignment; dictionary elements are not destinations. The update itself allocates nothing, introduces no binding, and cannot initialize an unread constructor field.

User-defined classes and enums can provide explicit mutable in-place hooks; see operator overloading. Those calls use the method’s implementation rather than the scalar read/compute/write sequence.

Several targets

target, target = value, value assigns to two or more targets at once:

def main() -> Result[(), AllocError | IoError]:
    mut items = [10, 20, 30]?
    items[0], items[2] = items[2], items[0]
    print(items)?
    mut low = 1
    mut high = 2
    low, high = high, low
    print((low, high))?
    Ok(())
[30, 20, 10]
(2, 1)

The right side is one tuple. With several targets its parentheses are optional, so a, b = b, a means a, b = (b, a); a single target still requires them. Its component count must equal the target count.

  1. Evaluate the whole right side.
  2. Assign each component to its target, from left to right, exactly as a single assignment to that target would.

Because the right side is complete before the first write, targets can exchange values. Each target evaluates its own indices when its turn comes, so an earlier target can change a later target’s index: i, items[i] = 2, 5 writes items[2]. A target is a name, a field, a collection element, *reference, or _, which destroys its component. A name follows the single-assignment rule: it assigns an existing mut local or declares a new immutable one. mut a, b = value declares mutable locals and accepts names or nested tuples of names only. Tuple targets nest recursively: (a, (b, c)) = value requires the same tuple shape. mut (a, b), c = ((1, 2), 3) and mut ((a, b), c) = ((1, 2), 3) declare the same nested components; the first tuple remains one target when an outer comma follows it. A singleton (a,) still requires one tuple component. Leaves are assigned left to right after the whole right side is evaluated; field, index and dereference leaves follow the same rules as flat targets. Naming the same local twice, starred targets, and a closure capture as a name target are rejected. In a constructor, a self.field target initializes its field, and later targets may read it.

The right side obeys ordinary ownership, so reading an owned element such as a nested list still cannot move it out of its collection. Exchange owned list elements with swap. When a target traps on a missing index or key, earlier targets have already been assigned. Nothing is allocated.

Replacing a field

replace(owner.field, replacement) follows the same evaluation order but returns the previous field value to the caller instead of destroying it. It requires the same type, visibility, mutability, and loan checks. It supports nested fields, boxed owners, and fields reached through collection elements. It allocates nothing and keeps the field initialized. It also accepts a list element and the target of a mutable reference, replace(*reference, replacement), but not a whole binding or temporary receiver. A user declaration named replace continues to resolve normally.

Use replace when the owner is reached through a reference or defines __del__, or when it must stay usable as a whole. A field of an owned binding can instead move out and be assigned later.