RFA-451 · Case file with fixtures · Case 423 of 694 · Compiler evidence
Rust or-pattern Bindings Must Use the Same Binding Mode
Matching alternatives that share an arm must bind each name in one consistent mode. Borrow in every alternative, move or copy in every alternative, or split the arms when their ownership policies really differ.
- 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
- The shared arm body is type-checked once, while ref and by-value binding modes give the same name different types and ownership relationships.
- First discriminating check
- Write the inferred binding type for each alternative, then borrow consistently, move consistently, or split ownership policies into separate arms.
I wrote (0, ref value) | (value, 0) and expected both alternatives to expose the non-zero component. Rust emitted E0409 because value is borrowed in the first alternative and bound by value in the second.
The failing fixture looks symmetric to a human, but its ownership is not symmetric. One route gives the arm body a reference; the other gives it a value.
One name must have one type in the shared body
An or-pattern selects one arm body. That body is type-checked once, so a binding such as value needs one stable type and one ownership relationship regardless of which alternative matched.
The E0409 explanation repairs the issue by using the same mode on both sides. In this example, both alternatives can say ref value.
Without the rule, an expression like consume(value) could move an integer on one path and only pass a reference on another.
Borrow consistently when later ownership matters
The repaired fixture uses:
(0, ref value) | (ref value, 0) => println!("{value}")
Now value has the same reference type in both alternatives. This is a small example with integers, but the decision matters more for String, buffers, and domain records.
If the matched value must remain usable after the match, I usually borrow in every route rather than moving from only one.
By-value can also be consistent
For Copy values, (0, value) | (value, 0) is often simpler. Each route copies the selected integer into one local. For non-Copy values, the same syntax may move the field.
E0409 is not saying borrowing is preferred. It says every alternative must agree. I choose the mode based on the caller's later needs, then apply it consistently.
Match ergonomics can hide the mode
The binding-mode rules can introduce references automatically when a non-reference pattern matches through a reference. This makes some inconsistent cases less visually obvious.
When the compiler reports E0409, I write down the scrutinee type and the inferred type of each binding. I do not infer ownership only from whether the word ref is visible.
Edition 2024 also tightens where explicit binding modifiers may appear after an implicit borrow. The surrounding reference structure matters.
Splitting arms is sometimes the honest repair
If one variant owns a value while another exposes borrowed state, forcing identical binding modes may create cloning or awkward lifetimes. I then split the alternatives:
Owned(value) => consume(value),
Borrowed(value) => inspect(value),
Two arms can call a common helper later if their results converge. The ownership difference remains visible at the boundary where it originates.
Mutability must agree too
Bindings across alternatives must not disagree about mutability or reference mutability. A body cannot treat one possible value as &mut T and another as &T under one name.
I decide whether mutation is part of the operation before combining alternatives. If only one representation can be mutated, the cases probably deserve separate control flow.
Generated matches need a binding signature
For generated parsers and protocol enums, I find it useful to define a small signature for each combined arm: name, type, and mode. Macro code should produce the same signature for every alternative.
That is a stronger test than comparing the text of the patterns. Two texts can differ while producing compatible bindings, and similar-looking texts can hide a mode mismatch.
My E0409 checklist
For reviews, I pay special attention when a quick fix adds ref to only satisfy the diagnostic. That changes what the body owns and can push a borrow farther through the function. Sometimes that is exactly right; sometimes the operation should consume the value in both alternatives. I follow the binding to its last use and check the state of the scrutinee afterward. Consistency across alternatives is mandatory, but the chosen consistent mode is still an architectural decision about ownership.
- What is the scrutinee type, including references?
- What exact type does the binding have in each alternative?
- Is each occurrence by value,
ref, orref mut? - Does match ergonomics introduce an implicit borrow?
- Must the original value remain usable afterward?
- Would splitting the arms express a real ownership difference?
- Is generated code enforcing one binding signature?
The core principle is that shared control flow needs a shared ownership contract. An or-pattern may discover the same concept in different structural positions, but the resulting local must mean the same thing to Rust and to the reader on every path.