Mehdi Akiki
Rust Failure Atlas / Upgrades and compatibility

RFA-085 · Case file with fixtures · Case 57 of 694 · Compiler evidence

Rust 2024 Match Ergonomics: Why mut Needs an Explicit Reference Pattern

The 2024 pattern rules make reference elision and explicit binding modifiers less ambiguous. Trace the default binding mode before deciding whether you wanted a copied value or a mutable reference.

Reviewed
Rust
Rust 1.98.1
Targets
all targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
The pattern uses match ergonomics to skip a reference while also requesting a by-value mutable binding, an ambiguity the 2024 rules reserve.
First discriminating check
Write the reference part of the pattern explicitly, then inspect the inferred binding type before copying the migration mechanically.

The following pattern is short, but Rust 2024 rejects it:

fn main() {
    let [mut value] = &[41];
    value += 1;
}

Rust 1.98.1 says it cannot mutably bind by value within an implicitly-borrowing pattern. The failing fixture records the exact diagnostic and suggestion.

The difficulty is that two different uses of “mutable” can be mixed in one line. A mutable local binding is not the same thing as a mutable reference to the matched input. Before applying the suggestion, I work out which one the code needs.

Match ergonomics can skip written reference patterns

When a non-reference pattern matches a reference, Rust's match ergonomics adjust the default binding mode. This lets common code avoid repeating & and ref everywhere. For example, matching [value] against &[41] can bind through the outer reference automatically.

Older edition rules also allowed some explicit modifiers after that implicit transition. The 2024 edition reserves combinations that were hard to reason about consistently. The Edition Guide describes the new restriction: mut, ref, and ref mut may appear only while the pattern prefix is fully explicit.

The Reference binding-mode section explains how matching a reference with a non-reference pattern updates the default mode.

The direct repair reveals the copy

For this example the compiler suggests writing the reference explicitly:

let &[mut value] = &[41];
value += 1;
assert_eq!(value, 42);

This is the verified repair. The outer & is now part of the pattern. value is a copied integer stored in a mutable local binding. Changing it does not change the original array.

That last sentence is easy to miss. The code compiles, but compilation alone does not prove the intended mutation happened.

If the goal is to mutate input through a reference, the data and pattern must provide a mutable reference:

let values = &mut [41];
let [value] = values;
*value += 1;
assert_eq!(values[0], 42);

Here value is a mutable reference. No mut value binding is needed because the referent, not the binding variable, is changed.

Type annotations are a useful migration tool

In a complex nested pattern I temporarily state the expected type:

let value: &mut i32 = value;

or:

let value: i32 = value;

This turns a vague question about pattern syntax into a concrete type check. I remove the extra annotation after the migration is clear, or keep it when it documents an important boundary.

Nested tuples, slices, and enums can change the default binding mode at different points. Reading only the final variable name is not enough. I trace the pattern from outside to inside and mark where each reference is consumed explicitly or ergonomically.

Why cargo fix still needs review

The edition migration lint can make the pattern fully explicit while preserving the old behavior. That is exactly what an automated migration should do. It cannot know whether the old behavior was what the author believed.

This case is a good example: preserving a copied mutable local may reveal that somebody expected to modify the source. I add or inspect an assertion about the source value, not only the local binding.

The rule applies beyond let statements

The same binding-mode reasoning appears in match, if let, while let, function parameters, and destructuring assignments. A migration may flag only one rare branch because that branch adds ref or mut inside a larger ergonomic pattern. I reduce the branch to its scrutinee type and one pattern before editing the full match. This is faster than trying several combinations of ampersands and accepting the first one that compiles.

Macro-generated patterns need another check. The edition used to interpret tokens belongs to the crate and context where those tokens are parsed, while a macro may have been authored elsewhere. I inspect the expansion and update the macro input or generator rather than patching one expanded-looking error repeatedly.

My practical rule

When a 2024 migration flags match ergonomics, I write down three facts:

  1. The type of the scrutinee.
  2. The default binding mode at the flagged identifier.
  3. Whether the code must mutate the binding, the referenced value, or neither.

Then I make enough of the reference pattern explicit that another reader can reach the same answer. I do not expand every pattern mechanically if the shorter form remains unambiguous, but I avoid clever mixtures of implicit borrowing and explicit modes.

Rust 2024 is not removing match ergonomics. It is drawing a clearer boundary around where explicit modifiers can override them. The repaired code should therefore do more than satisfy the parser: it should make ownership, copying, and mutation visible to the reader.