RFA-581 · Case file with fixtures · Case 553 of 694 · Compiler evidence
Rust Primitive Values Do Not Have User-Defined Fields
A primitive stores one language-defined value, not a domain record. Use the value directly or introduce a newtype when units and invariants deserve structure.
- 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 descriptive binding name was mistaken for declared record structure on a primitive value whose representation is fixed by the language.
- First discriminating check
- Use the numeric value directly or introduce a domain type when units, validation, and non-interchangeability justify real structure.
An integer is already the value; it is not a record containing a field named value. The failing fixture declares timeout_ms as u64 and attempts timeout_ms.value. Rust reports E0610 because primitive types have language-defined operations but no user-defined named fields.
Names on bindings do not change type structure
The identifier timeout_ms communicates a unit to the reader. It does not create a milliseconds property or wrap the integer in a domain object. Rust type structure comes from declarations and inference, not from variable-name conventions.
The E0610 explanation contrasts primitive values with structs. The immediate fix could simply use timeout_ms. If the surrounding code only needs the numeric milliseconds and the binding name keeps the unit obvious, that can be enough.
I introduce structure when the domain needs more protection than a name provides. Timeouts, identifiers, bytes, and counts are all commonly represented by integers but should not necessarily be interchangeable.
The repair makes the domain field real
The repaired fixture defines a Timeout struct with a value_ms field. Field access now matches declared structure and the unit stays attached to the type.
For a real API I would likely make that field private and provide constructors or conversion methods. A Timeout::from_millis constructor can reject zero or enforce a maximum. An as_millis accessor can expose the numeric value without permitting arbitrary invalid construction.
The standard library's Duration is often better than creating a custom timeout wrapper. It represents non-negative duration with established constructors and conversions. A domain newtype around Duration can still distinguish request timeouts from cache ages where mixing them would be harmful.
Primitive methods are not fields
Primitives do have inherent methods, such as checked arithmetic and byte-order conversions. These are invoked with parentheses because they compute behaviour. E0610 is specifically about trying to access a field.
Constants associated with primitive types use paths such as u64::MAX, not value-field syntax. Understanding the distinction between fields, methods, and associated items makes several neighbouring diagnostics easier: value.method may produce E0615, while value.unknown() may report no method found.
The Reference documents numeric types and their language behaviour. User code cannot add fields to u64, and extension traits add methods rather than changing its representation.
Newtypes buy safety but require conversion policy
A tuple newtype such as struct TimeoutMs(u64); is smaller than a named-field record and still creates a distinct type. I choose named fields when labels materially improve construction and inspection, and tuple fields when the wrapper has one obvious representation kept behind methods.
Once a type is distinct, arithmetic and conversions no longer happen automatically. This is often the benefit. I must decide whether two timeouts can be added, whether conversion to a narrower platform duration can fail, and whether formatting includes units.
I implement From only for infallible conversions that preserve meaning. I use TryFrom where ranges or invariants can reject values. A convenient .into() should not make milliseconds silently become seconds.
External scalar schemas need boundary translation
JSON and databases often expose domain quantities as bare numbers. Deserialising directly into u64 loses unit and validation context throughout the program. I parse at the boundary into the domain type, report invalid values there, and let internal functions require the stronger type.
This also improves searchability. A function accepting Timeout reveals intent better than one of many u64 parameters. Compiler errors then prevent exchanging a timeout, byte limit, and retry count.
I test zero, maximum accepted input, overflow during unit conversion, and serialization round trips. Field syntax compiling is only the beginning; the useful value is the invariant that the wrapper establishes.
My E0610 checklist
- What primitive type does the receiver actually have?
- Did a variable name create an expectation of structure that the type does not provide?
- Is direct use of the primitive clear and safe in this local scope?
- Would
Durationor another standard domain type fit better? - Should a newtype attach units, validation, or non-interchangeability?
- Are methods, associated constants, and fields being distinguished correctly?
- How do external numeric values enter the stronger internal type?
- Are conversion boundaries, overflow, and unit mistakes tested?
The core principle is that a primitive is one broad value domain. A field needs declared structure. I either use the primitive honestly or introduce a domain type whose extra syntax pays for itself with units, invariants, and safer interfaces.