Skip to main content
v0.13.0
rfc-0042implemented

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:

BeforeAfter
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.