WEP: Super Traits
Context
A trait cannot state that implementing it requires implementing another. Every prerequisite is therefore unwritten, and since Wado monomorphizes everything, each one surfaces as an error inside the library rather than at the impl that is actually wrong:
trait Circle {
fn radius(&self) -> i32;
fn describe(&self) -> String { return `r=${self.radius()} a=${self.area()}`; }
}
// A type implementing only `Circle`:
// error: no method 'area' found on type 'Blob' ← reported at Circle::describe
The same gap makes bounds redundant — T: Ord does not imply T: Eq, so
impl<T: Eq + Ord> Eq for RangeExclusive<T> spells both — and lets Ord and
Eq be implemented independently, so a type can have a working < and a
missing ==.
Decision
Syntax
Rust-compatible:
trait Ord: Eq {
fn cmp(&self, other: &Self) -> Ordering;
}
trait Circle: Shape + Display {
fn radius(&self) -> i32;
}
The clause sits between the trait's generic parameters and its block, and takes
the same bound list generic parameters do, so + lists and associated-type
constraints (trait A: B<Item = i32>) are included.
A fn(...) / fn mut(...) bound is rejected here: a callable signature is not a
trait a type can be required to implement.
Obligation
impl Sub for T requires T: Super for every trait in Sub's closure, not
only its direct supertraits — one satisfied structurally has no impl block of
its own to carry the rest of the chain. Reported at the impl block with a reason
chain. Structural on-demand derivation satisfies the obligation, so a plain
struct gets Ord: Eq without an impl Eq being written.
The impl answering that obligation is where the supertrait's members live, so
impl Sub for T neither owes nor may bind an associated type Super declares
(Associated Types). A binding written in
the subtrait lands where no projection through Super reads it.
Elaboration
A declared bound T: Sub expands to the transitive closure of Sub and its
supertraits, feeding bound checking, on-demand derivation, and method lookup
alike. The derivation half is what makes T: Ord alone sufficient for ==.
The expansion happens where a bound is read, not where it is registered: a type
parameter's bounds stay as written, and the question "does T implement Eq?"
is what walks the closure. Registration sites are too many to keep in step.
A clause writes its arguments in the declaring trait's own parameter space, so
trait Gauge<X>: Measure<X> says Measure at whatever Gauge was asked at:
T: Gauge<i32> supplies Measure<i32>, and impl Gauge<i32> for Ruler
requires Ruler: Measure<i32>. A position the clause leaves out stands at the
supertrait's declared default, so a bound inherited through it arrives as a type
rather than as a parameter no later reader can resolve.
A clause may write Self::Assoc as an argument. Self there is the
implementing type, so trait Constrained: Make<Self::Base> asks each impl for
Make at the associated type that impl binds: an impl binding Base to
String owes Make<String>, and one binding it to i32 owes Make<i32>. A
bound carries the projection with it, so T: Constrained requires
T: Make<T::Base>, and a call through it lands on the impl the projection
names (Trait Resolution).
The obligation an impl owes is read from what every impl on the target binds, not from what the block under check happens to write. A clause reached through another trait is therefore answered the same as one the block wrote itself: a question about a type is answered by the type.
The closure is stored in the declaring trait's parameter space, which is not the reading site's. Each clause therefore travels with the chain of clauses that reaches it, every step written in the space of the step before. A reader carries its own arguments down that chain, resolving each step in what the step before it answered, and reads the clause in the space it arrives at. A reader that takes a clause for one of its own bounds is the defect this forecloses. It fails by resolving a parameter name the site never wrote, which nothing downstream can detect.
Walking is what answers a projection, because no spelling denotes one:
trait Sink<X: Src>: Collect<Item = X::Item> read at Sink<Feed> binds Item
to what Feed binds it to. The walk settles X to Feed before anything reads
X::Item, so the base is a type by the time the projection is asked.
An associated-type constraint written in supertrait position
(trait Sink: Collect<Item = i32>) is checked: a type binding Item = String
is rejected wherever T: Sink is required, the same as if the constraint had
been written on the bound directly. It also answers the projection, so T: Sink
gives T::Item = i32 with no annotation. A constraint naming the writer's own
parameter (trait Sink<T>: Collect<Item = T>) is read at the argument the bound
writes, U: Sink<i32> giving U::Item = i32.
Cycles
A trait that reaches itself through supertraits is an error at its declaration.
Name collisions
A method reachable through more than one of a receiver's bounds is an error
where it is called, not where the traits are declared — Rust's E0034. One rule
covers all three shapes: a subtrait shadowing a supertrait method, two
supertraits of a diamond sharing one, and a <T: Left + Right> written by hand.
Rejecting at the declaration would cover only the first, and leave the other two
resolving silently to whichever bound came first.
The escape is to name the trait: Base::name(&x), the trait-qualified call
form from
WEP: Overload Resolution.
Standard library
trait Ord: Eq only. Every stdlib impl Ord already has a matching Eq, so the
obligation holds on arrival, and the redundant Eq in impl<T: Eq + Ord> comes
out.
Known gaps
Two standard-library clauses are unwritten: a shared face over the Reflect*
traits, and the stdlib Fn: FnMut that
Closure Implementation Internals
leaves open.
The trait solver states a clause's arguments only where it can name them as types. A clause whose argument is the subtrait's own parameter states none there, and the solver answers that edge at the supertrait's declared defaults. It is lenient rather than wrong — the elaborator, which does carry the argument, is what rejects a mismatch — so the two engines disagree on nothing a program can reach. The solver's type language has no term for a trait's own parameter.
Consequences
Removing a supertrait from a published trait breaks downstream code that relied on the implied bound; adding one breaks implementors. This is the trade Rust makes, and the reason a non-implied bound carries neither risk.
Iterator becomes decomposable — ExactSizeIterator / DoubleEndedIterator
style splits are expressible for the first time — though nothing is split here.
Front-end only: no NIR, WIR, codegen, runtime, or code-size effect.
