RFA-547 · Case file with fixtures · Case 519 of 694 · Compiler evidence
Rust Match Guards Cannot Mutate the Value Being Classified
A guard refines whether a pattern arm applies; it cannot rewrite the matched place mid-classification. Mutate after the arm has been selected.
- 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 guard refines a candidate pattern without restarting the match, so mutating its subject could invalidate earlier exhaustiveness decisions.
- First discriminating check
- Keep the guard observational and perform the state transition before matching or inside the selected arm body.
A match guard runs after a pattern appears to match but before that arm is finally selected. Mutating the matched place during this decision could invalidate the classification already under way, so Rust reports E0510.
The failing fixture matches state, reaches the Some pattern, assigns state = None inside its guard, and returns false. The matcher would then continue as if it were still considering the original value.
Guards refine patterns; they do not restart matching
An arm such as Some(value) if value > 3 => ... first checks the Some pattern and then evaluates the boolean condition. If the guard is false, matching continues with later arms.
There is no language rule saying that the match restarts from the first arm after a guard mutates its subject. Allowing that mutation could make earlier arms newly applicable and break exhaustiveness reasoning.
The official E0510 page describes this as needing to go “back in time” to reconsider an arm. The compiler prevents the inconsistent state instead.
The repaired code mutates after selection
The repaired fixture handles Some(value) and sets state = None in the arm body. At that point pattern selection is complete.
This produces a clean sequence:
- classify the current state;
- choose exactly one arm;
- perform that arm's state transition.
This sequence resembles a small state machine and is easier to test than classification with hidden mutation.
Read-only guards can still have effects
Rust guards are general boolean expressions, and some side effects that do not assign the matched place may compile. I still keep guards observational when possible. Logging, counters, or external mutation inside a guard can run for candidate arms that are later rejected, surprising readers.
The Reference's match guard section explains guard evaluation and notes how bindings are shared-borrowed for the guard.
A helper such as is_eligible(&value) communicates a predicate better than a block that changes unrelated state while returning a boolean.
Compute a decision before the match when needed
If arm choice depends on fallible or stateful work, I can compute that work first:
let eligibility = check(&state)?;
match (state, eligibility) {
// complete patterns
}
This gives the effect its own error boundary and makes the match operate on a stable snapshot. Another option is to match first and perform the fallible operation inside the selected arm.
I choose based on whether the work is meaningful for every state or only one variant.
Taking a value should happen outside its own match guard
Ownership code often wants to take() an Option while matching it. Calling state.take() changes the option to None, so doing it in a guard has the same conceptual problem.
I can match state.take() directly when consuming the option is the intended first action:
match state.take() {
Some(value) => use_value(value),
None => {}
}
Now the value being matched is the result of take, not the original place that was simultaneously under classification. The transition is explicit before matching begins.
Patterns and bindings already encode much filtering
Before adding a guard, I check whether a more precise pattern can express the condition. Enum variants, literal patterns, ranges, and destructuring can make exhaustiveness visible to the compiler.
Guards are necessary for arbitrary predicates, but the compiler does not generally use them to prove exhaustive coverage. The patterns reference shows the structural choices available.
My E0510 checklist
- What exact place is the match classifying?
- Does the guard assign to or take from that place?
- Can mutation move into the selected arm body?
- Should a transition happen before matching, with its result then matched?
- Is the guard a read-only predicate or a hidden action?
- Can a structural pattern replace the guard?
- Could a false guard make an earlier pattern applicable after mutation?
- Are state transition and arm selection tested separately?
The core principle is that matching needs a stable subject while it decides. I use guards to ask questions about that subject and perform state changes only before matching or after an arm has been chosen.