RFA-578 · Case file with fixtures · Case 550 of 694 · Compiler evidence
Rust as Casts Do Not Run Collection Conversions
A representation-level cast is different from a semantic conversion. Use constructors or conversion traits that expose allocation, multiplicity, and failure.
- 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
- Representation-level casting syntax was used for a semantic choice involving collection length, ownership, and possible allocation.
- First discriminating check
- State the intended collection shape and choose an explicit constructor, From or TryFrom conversion, or iterator collection path.
Rust's as syntax performs a defined set of low-level casts. It does not mean “convert this value somehow.” The failing fixture asks for u8 as Vec<u8>, and rustc reports E0605 because constructing a growable collection is not a primitive cast.
A byte and a vector differ in more than representation
A u8 is one numeric value. A Vec<u8> owns an allocation-related state with a pointer, length, and capacity. Turning one into the other requires a semantic choice: should the byte become one element, specify a length, encode a number as text, or contribute its raw representation to several bytes?
No cast syntax can choose correctly for every domain. The E0605 page limits as to primitive conversions and certain trait-object coercions. The Reference lists the supported type cast expressions.
The rejection keeps allocation and policy visible. A tiny operator should not silently decide collection size, ownership, encoding, or error handling.
The repair states the intended collection shape
The repaired fixture uses vec![byte]. This explicitly creates a one-element vector and asserts that shape. If I wanted seven zero bytes, the correct expression would be vec![0; 7], which has completely different meaning despite involving the same integer.
Other source types support other intentional conversions. An array may become a vector with Vec::from(array). A slice can be copied with to_vec. An iterator can be collected. A string can expose its encoded bytes with into_bytes. Each API communicates ownership and element semantics.
The From trait is appropriate for infallible, value-preserving conversions that an API chooses to support. TryFrom represents validation or range failure. Neither trait is invoked by as.
Numeric casts deserve their own caution
Even supported primitive casts can lose information. Converting a large integer to a smaller integer truncates according to Rust's documented rules. Floating-to-integer casts have defined saturation behaviour. Pointer casts operate under separate provenance and safety constraints.
Compilation success therefore does not mean a cast matches business meaning. For identifiers, money, sizes, and protocol fields I often use TryFrom so out-of-range input becomes visible. I reserve as for cases where the language-defined numeric behaviour is intentionally the contract and tests cover boundaries.
Lint policy can flag suspicious casts, but it cannot infer every domain invariant. A port number, Unicode scalar, and array length may all be stored in integers while needing different validation.
Conversions document ownership transitions
Collection creation may allocate and can be significant in a hot path. The chosen method tells me whether elements move, clone, or borrow. For example, collecting owned iterator items differs from cloning a slice. These costs matter when reviewing streaming systems and high-throughput parsers.
I do not replace E0605 with a chain of opaque .into() calls until types become invisible. In local code, an expected destination type can make into concise. At public or performance-sensitive boundaries, a named constructor often explains the policy better.
When several conversions are plausible, I add a domain method. Packet::from_payload_byte(byte) or encode_status(status) says why the collection exists, leaving room for headers, checksums, and validation later.
Protocol bytes need an encoding decision
A frequent version of this mistake appears when an integer must be sent over a network. A numeric value is not yet a byte sequence: I must choose byte width, signedness, and endianness. Methods such as to_be_bytes and to_le_bytes state that policy and return fixed arrays that can be appended to a buffer. Turning the number into a one-element vector would be wrong for values wider than one byte, while a narrowing cast could discard high bits.
Text is another distinct representation. Formatting the number 7 produces the encoded character byte for 7, not the numeric byte value seven. I keep these choices in named encoding functions and test known wire examples. The compiler can then check types while the tests protect the external format.
My E0605 checklist
- Is the requested operation a primitive representation cast or semantic construction?
- What collection length and element meaning are intended?
- Does conversion allocate, copy, clone, move, or borrow?
- Is there an established constructor,
From,TryFrom, or iterator path? - Could a supported numeric cast still truncate or saturate incorrectly?
- Would a domain-named conversion make encoding policy clearer?
- Are sizes and boundary values tested?
- Is the conversion happening in a hot loop where allocation should be measured?
The core principle is that similar-looking values are not necessarily representation-compatible. I use casts only for the narrow operations Rust defines as casts. For collections and domain values, I choose an API that makes construction, ownership, and possible failure visible.