RFA-572 · Case file with fixtures · Case 544 of 694 · Compiler evidence
Rust Inherent Method Names Must Be Unique Across impl Blocks
Separate inherent impl blocks extend one type-level namespace; they do not create overload sets. Give operations distinct semantics and names.
- 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
- Separate inherent implementation blocks extend one associated-item namespace rather than creating overload scopes from which Rust selects by signature.
- First discriminating check
- Find every inherent definition, decide which operation owns the contract, and give genuinely different policies distinct semantic names.
Rust lets me split inherent methods for one type across several impl blocks. This is useful for organising constructors, protocol operations, and bounds on generic types. The blocks still contribute to one associated-item namespace. Repeating the same method name therefore produces E0592.
The failing fixture defines Client::connect twice. File separation, block order, and different bodies would not make the call client.connect() resolve to one unique operation.
impl blocks organise code, not overload resolution
I sometimes encounter this after merging modules or adding a specialised implementation. It is tempting to think each impl is a separate scope. For inherent associated items, callers see the combined surface of the type.
Rust's implementation rules describe inherent implementations as associated items for the implementing type. The E0592 explanation shows that duplicate definitions remain invalid even when one appears in another block.
Rust also does not choose inherent methods by return type or perform traditional signature-based overloading. If two operations mean different things, their names should carry that difference.
The repair names the second policy
The repaired fixture keeps connect and calls it from reconnect. These operations have related but different contracts: one establishes a connection, while the other represents recovery or replacement of an existing one.
That distinction matters more in real systems. A first connection may initialise credentials and pools. Reconnection may apply backoff, preserve queued work, emit recovery metrics, or invalidate state. Calling both connect would hide policy even if Rust allowed it.
I avoid artificial names like connect2. I ask what makes the second operation different to its caller. Good names might be connect_with_timeout, reconnect, connect_unchecked, or connect_from_config, but each implies a contract that the implementation must honour.
Generic specialisation does not create a free inherent overload
Generic types can have methods available only under particular bounds, but overlapping definitions can still create ambiguity or duplication. Stable Rust does not provide arbitrary inherent specialisation based on whichever bound happens to fit best.
When behaviour varies by capability, a trait can model that capability. Trait method identity includes the trait path, so fully qualified syntax can disambiguate intentionally shared method names. This is different from placing duplicates in inherent blocks: each trait is an explicit contract implemented by the type.
I use traits when multiple types share caller-facing behaviour or when callers should be generic over it. I keep an inherent method when it is fundamental to this concrete type. I do not invent a trait only to work around E0592 if a clearer operation name solves the design problem.
Duplicate methods often reveal generated or conditional code
Macros and cfg attributes can make the duplicate hard to see. Two platform modules may both become active under a feature combination. Generated bindings may collide with a handwritten convenience method. A copied implementation can remain after a refactor.
My first diagnostic step is to search for the exact associated name and the type's implementations, including macro expansions and enabled features. Then I decide which declaration owns the contract. Mutually exclusive platform implementations need mutually exclusive cfg predicates and CI coverage for supported combinations.
For public APIs, renaming or removing one method can be a compatibility change. I check released documentation and downstream use. A deprecated forwarding method may help migration, but it still needs its own unique name relative to the replacement.
My E0592 checklist
- Which type owns every reported duplicate definition?
- Are the methods inherent, trait-provided, generated, or conditionally compiled?
- Did two source files or a merge create the same inherent item?
- What semantic difference should appear in the operation names?
- Would one method call a lower-level helper instead of duplicating logic?
- Is a real shared capability better represented by a trait?
- Are feature and target predicates truly mutually exclusive?
- Does a public rename require a compatibility path and documentation update?
The core principle is that a type should present one resolvable meaning for each inherent method name. Multiple impl blocks help humans organise an implementation, but they do not divide what callers see. I treat E0592 as an API-design signal and make the competing responsibilities explicit.