RFA-633 · Case file with fixtures · Case 605 of 694 · Compiler evidence
Pattern Syntax Must Match a Variant's Declared Shape
Tuple, struct, and unit variants carry different API shapes. Match with parentheses, real field names, or no fields, and choose named fields when payload roles need durable meaning.
- 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 local binding name was mistaken for a declared field name, or a tuple-to-struct refactor updated only one side.
- First discriminating check
- Inspect the variant declaration and mirror its unit, parenthesized tuple, or named-field shape while reviewing move versus borrow.
Rust data constructors have a declared shape. A tuple variant uses ordered fields, a struct variant uses named fields, and a unit variant carries none. Patterns mirror that shape. The failing fixture declares Message(String) but tries to match { text }, producing E0769.
A binding name does not invent a field
Inside Message(text), text is a new local binding for tuple field zero. The variant itself does not have a field named text. By contrast, Message { text } is shorthand for a struct field actually declared with that name.
The official E0769 explanation shows both a tuple pattern and the less common numeric-field struct syntax. The Reference describes patterns and how they destructure values.
The repaired fixture uses the parenthesized tuple pattern matching its declaration.
Shape is part of public API design
I use a one-field tuple variant when the payload meaning is obvious from the variant and type, such as Event::Disconnected(Reason). Named fields become valuable when several values share types or when roles need explanation: Message { channel, text } is harder to swap accidentally than Message(String, String).
Changing a tuple variant into a struct variant breaks construction and patterns even when payload types stay identical. In a public crate, this is a compatibility change. I choose the shape before release based on likely evolution and readability.
Adding a named field also breaks exhaustive construction and matching, which can be beneficial inside one codebase because every site must consider new data. For externally evolving wire formats, Rust enum shape is separate from serialization compatibility and needs explicit schema rules.
Patterns control ownership
Matching an owned enum by value can move its String out. Matching &event normally binds borrowed fields through match ergonomics. Using ref, ref mut, or @ patterns can make more complex ownership explicit.
When correcting E0769, I inspect the binding type. A syntactic fix might unexpectedly move data and prevent later use of the enum, or add a clone to silence that next error. Borrowing may better match an inspection operation.
The Book’s enum chapter connects variant declarations with match and payload modelling. I keep examples small enough that field movement is visible.
Generated patterns must share one schema model
Macros that generate both enum definitions and match arms should derive them from the same structured variant description. Generating declarations from one template and arms from another lets a tuple-to-struct change produce E0769 far from the schema.
I add compile tests for every generated shape: unit, single tuple, multiple tuple, named fields, generic fields, and cfg-controlled variants. Snapshot tests can show token changes, while compilation proves the tokens agree with Rust’s grammar and type model.
Serde field naming does not change Rust field shape. A tuple variant can have a particular wire representation, and a struct variant can be renamed externally. I avoid guessing the in-memory source form from JSON examples.
Match completeness is a separate guarantee
After fixing the shape, rustc may reveal non-exhaustive matching. I handle every domain state deliberately. A wildcard is useful for forward-compatible or irrelevant states only when it does not hide a new condition requiring action.
For public non-exhaustive enums, downstream callers need wildcard handling. Internally, exhaustive matches often give better change detection. Pattern guards refine selected variants but do not make an otherwise incomplete match exhaustive.
I test values at domain boundaries and confirm which payloads are moved, borrowed, or ignored. This turns a local punctuation repair into a reviewed state-handling contract.
My E0769 checklist
- Is the constructor declared as unit, tuple, or named-field form?
- Does the pattern mirror that exact shape?
- Are apparent names real fields or only new local bindings?
- Would named fields better communicate multiple payload roles?
- Does the corrected match move, borrow, or mutably borrow each payload?
- Is changing variant shape a public compatibility break?
- Do macros generate definitions and arms from one structured schema?
- After the repair, is the match intentionally exhaustive?
The core principle is that patterns are the inverse vocabulary of constructors. E0769 says those two shapes disagree. I match the declared form and then review the deeper contract: payload meaning, ownership, evolution, and exhaustive state handling.