RFA-538 · Case file with fixtures · Case 510 of 694 · Compiler evidence
Rust Reassignment Needs an Explicit Mutability or Transformation Choice
E0384 asks whether one identity changes over time or a new value is derived from an old one. Mutability and immutable transformation tell different stories.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- The code uses one immutable identity for two sequential values without deciding whether this is evolving state or a transformation stage.
- First discriminating check
- Use a small mut binding for genuine evolving state, or preserve semantic stages with distinct immutable names or deliberate short shadowing.
When Rust reports E0384, adding mut is syntactically easy. I first ask a more useful question: is this one value changing through time, or am I deriving a new fact from an old one?
The failing fixture initializes retries to two and then assigns three to the same immutable binding. Rust rejects the second assignment because bindings created with ordinary let are immutable.
Immutability belongs to the binding
The value 2_u8 is not itself “immutable” in an object-oriented sense. The name retries is bound to a value and cannot be assigned a different one unless declared let mut retries.
This distinction matters with ownership. I can move an owned value out of an immutable binding in allowed contexts, borrow it, or consume it through a method. What I cannot do is mutate through that binding or replace its value without the required permission.
The official E0384 page presents both common repairs: declare the binding mutable, or create a new binding through a new name or shadowing.
The repaired fixture preserves two meanings
The repaired fixture keeps configured_retries and computes effective_retries. Both values remain available and the names record why they differ.
This is the design I prefer for configuration normalization, request validation, currency conversion, and parsing. Raw and effective values are different stages of knowledge. Overwriting the first can make logs and debugging less useful.
The code now reads as a transformation rather than a state update. Rust's default immutability pushed the domain distinction into view.
Mut is correct for evolving local state
An accumulator, parser cursor, retry counter, or buffer often represents one state that genuinely evolves:
let mut attempts = 0;
while should_retry() {
attempts += 1;
}
Here separate names for every iteration would add noise. mut communicates a bounded mutation region and is entirely idiomatic.
I try to keep that region small. A mutable local inside one function is easier to reason about than mutable state shared across tasks. E0384 only asks for language permission; architecture determines whether widening mutation is wise.
The Rust Book's variables and mutability chapter explains why immutability is the default and how shadowing differs.
Shadowing creates a new binding
Rust also permits:
let retries = parse(raw)?;
let retries = retries.saturating_add(1);
The second let shadows the first. It can even change the type, which mutable reassignment cannot. Shadowing is useful for staged transformation where the intermediate is no longer needed and retaining one conceptual name helps readability.
I avoid long functions with many shadowed versions because error messages and reviews can become confusing. Distinct semantic names are stronger when both stages matter. Shadowing works best for a short, linear refinement.
Mutability does not bypass borrowing rules
Changing let value to let mut value permits mutation through the binding, but it does not allow mutation while an incompatible borrow remains active. Nor does it make a field mutable through an immutable shared reference.
If E0384 disappears and a borrow-checker diagnostic appears, I inspect reference lifetimes and ownership. These are layered conditions:
- the expression must identify a place;
- the binding or path must permit mutation;
- active borrows must permit exclusive access;
- the operation must exist for the type.
Applying every suggested keyword at once can hide which layer the design violated.
Interior mutability is not a shortcut for local E0384
Types such as Cell, RefCell, Mutex, and atomics allow mutation through shared-facing APIs under specific runtime or synchronization rules. Replacing a local immutable integer with one of them to avoid mut would increase complexity and change the failure model.
I choose interior mutability when ownership topology requires shared access and the type's rules match the domain. For ordinary sequential reassignment, a small mut binding is clearer.
The Reference's variables section is a compact source for binding, scope, and shadowing terminology.
My E0384 checklist
- Is this a second assignment or the first delayed initialization?
- Does one conceptual state evolve, making
muthonest? - Is a new value being derived, deserving another name?
- Would short linear shadowing improve or obscure the transformation?
- Must the original value remain available for logs or validation?
- Is an active borrow the next constraint after mutability?
- Am I considering interior mutability for a real ownership reason?
- Can I reduce the mutable region to one local scope?
The core principle is that reassignment is a modelling choice as well as a keyword choice. I use mut for genuinely evolving state and new bindings for transformations whose before-and-after meanings deserve to remain visible.