let mut for Mutable Bindings
Summary
Replace standalone mutable binding declarations with let mut declarations.
// Before
mut counter = 0;
pub mut cache: Int[] = [];
// After
let mut counter = 0;
pub let mut cache: Int[] = [];
The mut keyword remains the marker for mutability, but let becomes the only keyword that introduces value bindings. This makes mutable and immutable declarations one syntactic family:
let name = "Ada";
let mut counter = 0;
Motivation
Metel currently uses two binding introducers:
let value = 1;
mut counter = 0;
This makes mut serve two roles:
- declaration introducer for mutable bindings
- mutability modifier in other positions, such as
mut self,*mut T, and&mut x
Using let mut keeps mut consistently modifier-like. A binding is introduced by let; mutability is an attribute of that binding. This also matches the shape used by Rust and makes declarations easier to scan, especially beside pub:
pub let value = 1;
pub let mut counter = 0;
Design
Binding Declarations
The canonical syntax for value bindings becomes:
let IDENTIFIER ( ":" Type )? "=" Expression ";"
let mut IDENTIFIER ( ":" Type )? "=" Expression ";"
let creates an immutable binding. let mut creates a mutable binding.
let x = 1;
let y: Int = 2;
let mut count = 0;
let mut total: Int = 0;
The mutability semantics are unchanged:
- immutable bindings cannot be assigned after initialization
- mutable bindings can be assigned after initialization
- all bindings must be initialized at declaration
- type annotations remain optional
- binding visibility and shadowing rules are unchanged
Public Bindings
For top-level public bindings, pub continues to prefix the declaration:
pub let version = "0.8.0";
pub let mut global_counter = 0;
pub mut name = value; is replaced by pub let mut name = value;.
For-Loop Initializers
C-style for loop initializers use the same declaration syntax:
for (let mut i = 0; i < 10; i += 1) {
// ...
}
The old for (mut i = 0; ... ) form is replaced by for (let mut i = 0; ... ).
For-In Bindings
For-in loop bindings remain immutable by default:
for (let item in items) {
// item is immutable
}
This RFC also allows a mutable iteration binding:
for (let mut item in items) {
item = normalize(item);
}
The mutable iteration binding only permits reassignment of the loop-local binding. It does not mutate the collection element in place.
Other Uses of mut
This RFC does not change other uses of mut:
fun increment(mut self) { ... }
let p: *mut Int = &mut counter;
mut self, *mut T, and &mut x keep their current spelling and semantics.
Grammar Changes
The declaration grammar changes from separate LetDeclaration and MutDeclaration forms to one binding declaration with an optional mut modifier:
BindingDeclaration -> "pub"? "let" "mut"? IDENTIFIER ( ":" Type )? "=" Expression ";"
The C-style for initializer accepts a binding declaration:
ForInit -> BindingDeclaration | ExpressionStatement | ";"
The for-in grammar permits the same mutability modifier on the loop binding:
ForInStatement -> "for" "(" "let" "mut"? IDENTIFIER "in" Expression ")" Block
Migration
The migration is mechanical:
| Before | After |
|---|---|
mut x = value; | let mut x = value; |
mut x: T = value; | let mut x: T = value; |
pub mut x = value; | pub let mut x = value; |
for (mut i = 0; cond; step) | for (let mut i = 0; cond; step) |
Resolved Decisions
D1 - Standalone mut is dropped immediately
mut x = value; becomes a parse error as soon as this RFC is implemented. The language keeps only one binding introducer, let, and does not carry a transition alias.
D2 - Mutable for-in bindings are included
This RFC includes for (let mut item in items) so that loop-local bindings use the same mutable-binding syntax as ordinary declarations. The binding itself is reassignable; this does not imply in-place mutation of the iterated source element.
D3 - Initial implementation keeps the current AST split
The parser may lower let mut into the existing mutable-declaration node shape for the initial implementation. A later internal cleanup may merge declaration nodes if that removes real complexity, but this RFC does not require that refactor.
Decision
Outcome: Accepted Target: (pending milestone assignment)
The user-visible syntax and migration behavior are resolved here. Remaining work is implementation and follow-through in examples, tests, and the spec.
Coverage Checklist (added 2026-08-19, not part of the original RFC)
Retroactive breakdown of this RFC's distinct, fixture-testable normative claims, as headed sections for citation purposes only. The document above is unchanged and remains the historical record. Deliberately excludes claims that aren't independently observable from a program's behavior -- implementation strategy, design rationale, or internal architecture discussion belongs in the RFC's own prose, not here.
1. let creates an immutable binding
A let binding must be initialized and cannot be assigned after initialization.
Its type annotation is optional when the initializer supplies a type.
2. var creates a mutable binding
The current mutable-binding spelling is var name = value;. A var binding may
be reassigned after initialization; the historical let mut and standalone
mut declaration spellings are not current binding syntax.
3. C-style for initializers accept mutable bindings
A C-style loop may declare its counter with var, and that loop-local binding
may be reassigned by its step expression or loop body.
4. A for-in binding may be mutable without mutating its source element
for (var item in values) permits reassignment of item for that iteration.
Changing the loop-local binding does not write the replacement back into
values.
5. A for-in binding may be declared with var
A for-in loop's binding may be introduced with var, distinct from claim 4's
dynamics half (that reassigning it doesn't write back to the source): this is the
legality half, that var is accepted there at all.