Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-533 · Case file with fixtures · Case 505 of 694 · Compiler evidence

Higher-Ranked Rust Lifetime Bounds Use One Quantifier per Predicate

Higher-ranked bounds say a predicate holds for every chosen lifetime. Lifetimes governing one predicate belong in one for binder rather than nested binders.

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 two binders split the quantification of one logical predicate into an unsupported nested syntax.
First discriminating check
State the capability aloud as for every lifetime, gather all lifetimes for that predicate into one for binder, and verify their intended independence.

Higher-ranked trait bounds became less mysterious for me when I read for<'a> as “the caller may choose any 'a,” not as a lifetime being created and stored somewhere. E0316 then becomes mostly a grammar correction around one logical statement.

The failing fixture writes for<'a> &'a T: for<'b> Relates<'a, 'b>. Two binders are nested around pieces of the same predicate, and Rust does not support that form.

The quantifier belongs around a complete bound

The intent is that for every 'a and every 'b, &'a T implements Relates<'a, 'b>. Rust spells that with one binder:

for<'a, 'b> &'a T: Relates<'a, 'b>

The repaired fixture uses exactly this form. It collects all lifetimes quantified for the predicate before the left-hand type.

The official E0316 page distinguishes two legal placements: a quantifier over the trait bound or a quantifier over the whole clause. Combining both placements into a nested quantifier is the rejected shape.

For every is stronger than for one chosen lifetime

An ordinary function lifetime parameter is selected for a particular call. A higher-ranked bound requires the implementation to work no matter which suitable lifetime is selected later.

This commonly appears with callbacks:

F: for<'a> Fn(&'a str)

The callback cannot be tied only to one preselected borrow. It must accept a string reference for any call-local lifetime. That is useful when a function repeatedly borrows temporary views and invokes the same callback.

The Reference explains this meaning in higher-ranked trait bounds.

Binder placement controls where names are visible

In for<'a> &'a T: Trait<'a>, 'a is visible across the type and trait parts of the predicate. In T: for<'a> Trait<'a>, it is scoped to the trait bound on the right. Both can be useful.

I choose the smallest binder scope that covers every occurrence. When two lifetimes appear across both sides, the whole-predicate form is usually easiest to read. I never distribute binders according to visual proximity alone, because the logical scope matters more than formatting.

This is not a request to add static

Replacing a higher-ranked requirement with Trait<'static> changes “works for every borrow” into “works for this one program-long lifetime.” It often rejects useful closures or permits an interface with very different retention assumptions.

'static is not a universal lifetime wildcard. It is a specific strong bound. When the operation handles short-lived borrowed inputs, a higher-ranked contract is frequently the accurate one.

The Rustonomicon HRTB chapter shows why closure bounds need this “for all lifetimes” form even though the compiler often infers it in simple cases.

Multiple lifetimes may vary independently

for<'a, 'b> permits the caller to choose each lifetime independently. That is stronger than forcing them to be the same lifetime. I check whether the trait really needs all combinations or whether one shared 'a describes the domain.

In parsers, visitors, and adapters, one lifetime may describe a borrowed receiver while another describes an input view. Independent quantification can be intentional. If the implementation only works when one outlives the other, that relationship belongs in the predicate and tests.

Making the syntax compile is only the start; the quantifier must express the capability the API genuinely provides.

HRTB errors often surface in generated bounds

Macro-generated where clauses and elaborate type aliases can accidentally insert a second for around an already quantified fragment. I inspect expanded code or reduce the bound to a minimal function before editing a public trait.

A useful reduction keeps the trait declaration, one generic function, and the exact where predicate. If the reduced form emits E0316, I know the issue is binder syntax rather than a missing implementation.

My higher-ranked bound checklist

  • What complete proposition must hold for every lifetime?
  • Are the lifetimes chosen per call or fixed on the surrounding type?
  • Can all lifetimes for one predicate sit in for<'a, 'b>?
  • Which occurrences must be inside the binder's scope?
  • Do 'a and 'b vary independently or have an outlives relation?
  • Did a macro nest a binder around an already quantified fragment?
  • Am I incorrectly replacing “for every lifetime” with 'static?
  • Does a small compile fixture demonstrate the intended implementation?

The core principle is that a higher-ranked binder quantifies a complete capability. I gather the lifetimes for that capability into one binder, then read the bound aloud as “for every” to verify its meaning.