Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-539 · Case file with fixtures · Case 511 of 694 · Compiler evidence

Cyclic Rust Supertraits Have No Foundational Contract

Supertrait edges express prerequisites and need a direction. Mutual prerequisites form a type-level dependency cycle rather than two equivalent capabilities.

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
Mutual prerequisite edges have no foundational predicate from which trait well-formedness can be established.
First discriminating check
Draw the dependency graph, extract a real shared base, or compose independent capabilities with A + B at the use site.

A pair of traits can be related without either one being a prerequisite of the other. E0391 helped me distinguish “these capabilities often appear together” from “this capability logically requires that one.”

The failing fixture declares Reads: Writes and Writes: Reads. To understand either trait, rustc must first understand the other, so the dependency has no base.

A supertrait arrow has direction

trait Reads: Resource means every Reads implementor must also satisfy Resource. Generic code with T: Reads may rely on resource operations without adding a second bound.

This is not merely a grouping syntax. It adds an obligation to implementations and affects what can be assumed through generic and dynamic interfaces.

The Reference defines supertraits as traits that a trait's implementors must also implement. I draw the relationship as an arrow from the more specialised capability toward its prerequisite.

Mutual arrows do not mean equivalence

If reads always require writes and writes always require reads, it may appear that two arrows express equivalence. In the type system they create recursive prerequisites:

to prove Reads, first prove Writes
to prove Writes, first prove Reads

There is no foundational predicate from which the compiler can start. The official E0391 explanation calls this a type dependency cycle and uses mutually recursive supertraits as its central example.

If two names truly describe the same capability, one trait may be enough. If they are distinct but commonly bundled, I model that separately.

The repaired graph has a shared foundation

The repaired fixture introduces Resource. Both Reads and Writes depend on it, but neither depends on the other. A function that needs both writes T: Reads + Writes.

The graph now says three precise things:

  • resource identity is common foundation;
  • reading can exist without writing;
  • the particular operation needs both capabilities.

This is more flexible for read-only replicas, immutable snapshots, append-only sinks, and permission-scoped handles.

A convenience trait can bundle capabilities one way

Sometimes many APIs genuinely use the same combination. I can introduce a one-directional marker:

trait ReadWrite: Reads + Writes {}

impl<T: Reads + Writes> ReadWrite for T {}

Now ReadWrite requires both, while neither base trait requires ReadWrite. The dependency graph remains acyclic. The blanket implementation makes qualifying concrete types participate automatically.

I use this only when the name improves public APIs. A local T: Reads + Writes bound is often clearer than another trait layer.

Trait cycles may hide behind aliases and associated types

E0391 is broader than the tiny fixture. Rust's compiler evaluates semantic questions through queries, and one query can depend on another. Recursive type aliases, constants, predicates, and associated-type normalization may create cycles that travel across several declarations.

The rustc dev guide query chapter explains the query model behind many compiler computations. I do not need compiler internals to fix every cycle, but the model gives a useful debugging tactic: follow the reported dependency chain until an edge points back to an earlier node.

The correct repair breaks a semantic dependency, not merely moves definitions to a different file.

Module order does not solve a type dependency cycle

Rust is not complaining that Reads was textually declared before Writes. Swapping them leaves the same graph. Forward references are normal; recursive obligations without a well-founded meaning are the problem.

This matters when an error appears after a refactor. Reordering imports or modules may change which message appears first, but it cannot create a base contract. I reduce the graph and decide which relationship was reversed, redundant, or missing an intermediate abstraction.

Capability design benefits from weaker base traits

A foundational trait should usually promise only what every specialised implementor can honestly provide. Making Reads require Writes rules out read-only values throughout the program even if only one function needs mutation.

Smaller independent traits compose at use sites and often make testing easier. Strong bundled traits are useful at stable architectural boundaries, but cyclic requirements are a sign that the capability hierarchy has lost direction.

I ask whether the relationship is “is a prerequisite of,” “is often paired with,” or “is needed by this operation.” Each sentence maps to a different Rust design.

My E0391 cycle checklist

  • What exact nodes appear in rustc's reported cycle?
  • Which supertrait edges point back to an earlier trait?
  • Is either capability truly a prerequisite of the other?
  • Do both share a smaller foundational trait?
  • Should the use site simply require TraitA + TraitB?
  • Would one directional convenience trait help repeated bounds?
  • Is a type alias, projection, or constant extending the cycle indirectly?
  • Have I tested the reduced graph rather than only reordered declarations?

The core principle is that type-level dependencies need a foundation. I use supertraits for one-way prerequisites, independent bounds for composition, and a shared base when several capabilities inherit the same real contract.