RFA-505 · Case file with fixtures · Case 477 of 694 · Compiler evidence
Rust Assignment Cannot Write to a Constant Item
A Rust const is a named compile-time value and does not provide mutable storage. Put changing state in a mutable local, field, synchronized static, or owning abstraction with an explicit update API.
- 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
- A const is a named compile-time value that may be inlined, so it does not identify runtime storage into which assignment can write.
- First discriminating check
- Determine whether changing state belongs in a local, field, or synchronized shared owner, while keeping immutable defaults separate from current configuration.
I declared const RETRIES: u8 = 3 and later wrote RETRIES = 4. Rust emitted E0070 because a constant item names a value; it is not a mutable storage cell.
The failing fixture resembles assignment in languages where a “constant” may mean only a protected variable. Rust's const has a different model.
A const value can be used without one address
The Reference on constant items describes constants as values that are inlined wherever used. They do not necessarily have a unique address or mutable allocation that an assignment could target.
The left side of assignment must be an assignee expression representing storage. The place-expression Reference lists locals, statics, dereferences, indexes, and fields among relevant place forms.
RETRIES resolves to the constant's value, so there is no destination to overwrite.
The repaired fixture creates runtime state
The repaired fixture uses let mut retries. This binding owns runtime storage, and the assignment updates that storage before the assertion reads it.
This is right when the value is local to one operation. If changes must persist in a struct, I store the field on an owned mutable value. If changes must be shared across threads, I use an appropriate synchronization abstraction.
Choosing storage duration and ownership is part of the repair.
Static and const are not interchangeable
A static item has a fixed storage location, while a const is a named value. But ordinary immutable statics still cannot be assigned to, and mutable global state carries serious safety and synchronization concerns.
I do not replace const with static mut merely to make E0070 disappear. Direct mutable statics require unsafe handling and are usually inferior to atomics, locks, or state owned by an application component.
The desired access pattern determines the abstraction.
Configuration should not masquerade as a compile-time constant
Retry policy often comes from deployment configuration or changes at runtime. Encoding it as const states that it is fixed in the program's compiled logic.
I keep defaults as constants when useful, then copy the chosen initial value into owned configuration state. Updates act on that state. This distinguishes an immutable default from the current mutable setting.
Names such as DEFAULT_RETRIES and config.retries make the difference visible.
Assignment to other value expressions fails similarly
The official E0070 page also shows literals, function items, and type-level field paths used incorrectly on the left. They produce or name values and definitions, not writable instance storage.
For a struct field I need an instance, such as settings.retries = 4, and that instance path must be mutable. For a function result I need to decide whether I am updating a local copy or should obtain mutable access to the owner.
Interior mutability still has an explicit API
Types such as Cell, RefCell, Mutex, and atomics can change state through shared handles under their own rules. I call methods like set, borrow_mut, lock, or store; I do not assign to the wrapper's immutable binding.
This makes runtime checks, locking, or atomic ordering visible. Interior mutability changes how access is mediated, not the grammar of a constant item.
Shadowing creates a new binding rather than changing the const
A local let RETRIES = 4 inside a scope can shadow a constant name, subject to naming lint conventions, but it creates a separate binding. It does not update the constant seen elsewhere. I rarely use this pattern because identical names make ownership hard to see.
For staged transformations, ordinary shadowing with lowercase locals can be useful: let retries = DEFAULT_RETRIES; let retries = validate(retries)?;. Every line creates a new value, while assignment updates existing storage. Knowing which operation I need prevents an accidental global-state design.
My E0070 checklist
- Does the left path resolve to a const, static, function, type, or actual place?
- Which owner should retain the changed value?
- Is the state local, instance-owned, or shared?
- Am I confusing a compile-time default with current configuration?
- Would a mutable field or local be sufficient?
- Does shared mutation need an atomic or lock?
- Am I tempted to introduce
static mutonly to silence the error? - Does the repaired test observe persistence in the intended owner?
The core principle is that assignment writes to storage. A Rust constant supplies a reusable value, not a cell, so changing state needs an explicit runtime owner and mutation boundary.