RFA-383 · Case file with fixtures · Case 355 of 694 · Compiler evidence
Self as a Supertrait Type Argument Breaks dyn Compatibility
A bound such as Service: Tagged<Self> bakes the concrete implementer into the inherited trait instantiation. Once Self is erased behind dyn Service, that supertrait relationship cannot form one stable dynamic interface.
- 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
- The supertrait instantiation depends on the erased concrete Self type, so one dyn Trait interface cannot name the required inherited trait relationship.
- First discriminating check
- Reduce the supertrait argument to Self, then redesign the relationship so the dynamic supertrait does not depend on the erased implementer type.
I once reduced a dyn-compatibility error until no methods remained. The cause was entirely in the supertrait list: trait Service: Tagged<Self>. Rust reported that the trait uses Self as a type parameter.
The failing fixture proves that an empty trait body can still be incompatible with dyn because inherited relationships are part of its interface.
A supertrait contributes requirements and operations
If Service: Tagged<Self>, every Service implementer must implement one particular instantiation of Tagged: the argument is its own concrete type. Local therefore implements Tagged<Local>.
This is precise and useful in static generic code. Given T: Service, the compiler knows the relationship T: Tagged<T> and can select implementations using that concrete T.
Behind dyn Service, the concrete implementer has been erased. The required supertrait instantiation would be Tagged<hidden concrete type>, and one trait-object type cannot write that hidden argument as a stable part of its interface.
The dyn-compatibility rules include this case explicitly: using Self as a type parameter in the supertrait list is incompatible.
Replacing Self with dyn Service changes the meaning
It may be tempting to write Tagged<dyn Service>. That requires every implementer to support tagging the trait-object type, not itself. Tagged<Local> and Tagged<dyn Service> are different trait instantiations with potentially different methods and implementations.
This substitution is valid only if the domain relationship was truly about erased services. I do not use it as a syntax patch without checking callers.
The repaired fixture removes the generic supertrait argument entirely. Tagged becomes a simple dyn-compatible supertrait because that is enough for its example contract.
Move static relationships to a static layer
If Tagged<Self> expresses a real compile-time law, I keep it on a generic trait that is not promised to support trait objects. A smaller runtime trait can expose the operations needed dynamically.
Concrete types implement both layers, and an adapter constructs the dynamic view. This avoids weakening the static law merely to force one trait to serve every form of polymorphism.
An extension trait is another option. It can require Sized and provide methods based on Tagged<Self>, while the base service trait remains object compatible.
Associated types may express a different relationship
Sometimes the generic argument exists only to identify one related type. An associated type can say that each implementation chooses a tag type: trait Service { type Tag; }.
Dynamic callers then need that associated type specified where the trait-object rules permit it, for example dyn Service<Tag = String>. This groups objects sharing one tag type, not objects whose tag type must equal their own hidden concrete type.
The rewrite is useful when it matches the domain. It is not mechanically equivalent to Tagged<Self>.
Supertrait chains deserve the same audit as methods
E0038 often appears after adding a method, so developers scan only function signatures. I inspect the complete transitive supertrait graph. Every supertrait must itself be dyn compatible, and the way it is instantiated matters.
A remote dependency update can change that graph and break local trait-object uses even when the local trait body is untouched. Compile tests that construct the object catch this API property.
I reduce a complex failure by replacing supertraits one at a time with empty local traits. Once the object compiles, I restore bounds and type arguments until the exact relationship is visible.
This is a type-identity problem, not a size problem only
Adding ?Sized to the generic parameter of Tagged allows Self to appear syntactically without an implicit Sized complaint. The failing fixture already does this. Dyn compatibility still fails because the erased identity remains part of the supertrait instantiation.
That distinction prevents a common dead end: relaxing size bounds cannot recover type information erased by the object.
My API question is which relationships survive erasure
Before promising dyn Service, I list the facts dynamic callers need. Receiver methods and fixed associated-type equalities may survive. A law tying an inherited generic parameter to the hidden concrete Self usually belongs to static use.
The core principle is that trait objects erase more than layout; they erase the implementer's name at the caller boundary. Super<Self> carries that name into a required trait instantiation. Rust rejects the object because one dynamic interface cannot preserve a different hidden type argument for every value without an explicit representation for it.