RFA-643 · Case file with fixtures · Case 615 of 694 · Compiler evidence
Struct Update Syntax Needs a Real Base Value
The double-dot in a struct expression copies or moves remaining fields from one value; it is not an implicit default. Name the base, use Default explicitly, and review partial moves.
- 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
- Struct update punctuation was mistaken for an implicit default rather than a field transfer from a named value.
- First discriminating check
- Choose and name the real base or call Default explicitly, then review non-Copy moves and cross-field invariants.
In a struct expression, ..base says where every field not written explicitly comes from. Bare .. does not mean default values and cannot invent the missing fields. The failing fixture omits the base after the dots and receives E0797.
Update syntax is an expression with a source
The official E0797 explanation adds an existing struct value after the dots. The Reference defines functional update syntax and how unspecified fields are taken from that base.
The repaired fixture creates named defaults, overrides retries, and retains the timeout from the base. The assertion verifies the intended inherited field.
I name the base according to its role—defaults, previous state, or template—because the source of omitted configuration is important operational information.
Default must be explicit
If the type implements Default, Settings { retries: 3, ..Settings::default() } is a clear construction. The type’s default should represent one coherent context-free baseline. If different environments need different policies, named constructors or configuration layers are safer.
Deriving Default because update syntax is convenient can create bad production values: zero timeout, empty endpoint, or unlimited retries. I review every field and test the baseline.
Builder APIs can validate combinations before construction. They are valuable when fields have dependencies or construction is fallible, while ordinary struct update is ideal for simple value-like configuration.
Fields may move from the base
For non-Copy fields, update syntax moves omitted fields out of the base. Explicitly provided fields are not moved from it. This can leave the base partially moved and unusable as a whole, though unmoved accessible fields may still be usable under Rust’s rules.
The Book’s section on creating instances with update syntax demonstrates this ownership effect.
I do not add clones automatically to keep using the old value. If both configurations need independent ownership, cloning may be correct and its cost should be visible. If the old value is obsolete, movement expresses that transition well.
Update preserves type, not validation history
Struct update requires the same struct type. It does not re-run a constructor that originally checked relationships between fields. Overriding one field can invalidate an invariant involving an inherited field if fields are publicly constructible.
For invariant-bearing types I keep fields private and expose methods that validate transitions. A configuration struct may remain public and validate later at a boundary, but code should know which stage it represents.
Typestate can encode some transitions in distinct types, though it increases API surface. I use it when invalid intermediate states cause real risk, not for every optional setting.
Schema evolution affects update sites
Adding a private field can preserve some construction patterns within the defining crate while breaking external literals. Non-exhaustive public structs prevent downstream construction and update in ways that support evolution.
Within a service, update syntax can make new fields silently inherit defaults. That may be intended, but security-sensitive settings deserve tests when fields are added. An allowlist, TLS mode, or data-retention flag should not acquire an unreviewed baseline.
Serde update or merge behaviour is separate from Rust struct update. Configuration precedence across files, environment variables, and commands needs explicit merge semantics, including whether absence means inherit, clear, or reject.
When reviewing configuration diffs, I print or log only non-secret effective choices and their source layer. Two values built with update syntax may look almost identical while inheriting a critical timeout from different bases. Naming the base in code and preserving source information during parsing makes production diagnosis much easier than reconstructing precedence from a final struct.
My E0797 checklist
- Which base value should supply every omitted field?
- Was bare
..incorrectly assumed to meanDefault::default()? - Does the type have one safe, meaningful default?
- Which omitted non-Copy fields move out of the base?
- Will later code incorrectly expect the entire base to remain usable?
- Can overriding one field violate an invariant tied to inherited fields?
- Should a named constructor or builder validate the composition?
- Do tests review newly added security or operational fields and their inherited policy?
The core principle is that struct update is a transfer from an actual value, not a request for unspecified magic. E0797 requires the source. I name that source, then review defaults, moves, invariants, and configuration evolution explicitly.