Mehdi Akiki
Rust Failure Atlas / Runtime, memory, and library APIs

RFA-589 · Case file with fixtures · Case 561 of 694 · Compiler evidence

Rust Outlives Bounds Must Connect Input and Output Lifetimes

Two named lifetimes are independent until a bound relates them. Add the precise outlives proof, unify roles, or redesign the borrowed result.

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
Two named lifetime roles were assumed to be ordered even though no bound states that the input remains valid throughout the output region.
First discriminating check
Map the owners and uses, then add the precise input-outlives-output edge, unify identical roles, or return owned data.

Giving two lifetimes different names does not tell Rust which one lasts longer. In the failing fixture, a trait operation shortens a borrow from 'input to 'output, but the selecting function never proves that the input is valid for the output duration. Rust reports E0623.

Lifetime parameters begin as independent choices

The signature introduces 'input and 'output as caller-selected regions constrained only by how they appear. Human names suggest a relationship, but the compiler does not infer “input must last through output” from English.

The E0623 explanation presents two main solutions: add an outlives relationship or use one lifetime where the roles are actually the same. This is the key design question behind the diagnostic.

The notation 'input: 'output reads as “'input outlives 'output.” A reference valid for the longer region can be shortened to the smaller one.

The repair states the missing proof

The repaired fixture adds 'input: 'output to select. This matches the Convert implementation, which can return a shorter borrow from a longer one. The bound does not extend value; it rejects callers that cannot already satisfy the relationship.

This distinction matters. An outlives bound is a requirement, not a memory-management operation. It cannot keep stack data alive, change drop order, or turn a request-local reference into static storage.

If input and output are always intended to be the same borrow, one lifetime name may communicate better. Separate names plus a bound are useful when the output may validly be shorter, for example returning a view scoped to a transaction from data owned by a longer-lived store.

Type outlives bounds can appear too

Bounds such as T: 'a mean that values of T contain no references that become invalid before 'a. This is different from 'long: 'short, which directly relates two lifetime regions. Generic containers and trait objects can require both kinds of reasoning.

The Reference documents lifetime bounds. I expand type aliases and associated types when a diagnostic is hard to read, because an implicit reference inside T may be the real source of the requirement.

I also avoid adding 'static as a universal escape. T: 'static means the type has no borrowed data shorter than static; it does not mean a particular value must be stored forever. Requiring it can unnecessarily reject borrowed but safe callers.

Variance controls which shortening is allowed

Shared references are covariant over their lifetime, which supports using a longer-lived shared reference where a shorter one is required. Mutable references have stricter variance interactions because allowing arbitrary substitution could violate exclusive access or stored-value validity.

The Rustonomicon's subtyping and variance discussion is useful for advanced generic cases, but I first reduce the problem to concrete owners and uses. Variance tables are easier to apply after the domain relationship is understood.

Types containing function parameters, mutable references, or interior mutation may not shorten in the direction intuition expects. PhantomData also affects variance and drop checking even though it stores no runtime reference. Low-level wrapper authors should choose its marker type deliberately.

A lifetime graph makes complex signatures reviewable

For several regions, I draw nodes for owners and edges for required outlives relations. I mark where references are created, stored, returned, and last used. Cycles often mean roles should be one lifetime; missing edges show why conversion is unsafe.

Then I ask whether the API needs to expose this graph. Returning owned data or accepting a callback scoped to the borrow can localise relationships. Complex lifetime signatures are justified in zero-copy and systems interfaces when they provide measured value, not merely to avoid one allocation by habit.

Compile-fail fixtures protect negative guarantees, while ordinary tests check behaviour under valid nested scopes. Both matter for a public unsafe-adjacent abstraction.

My E0623 checklist

  • Which owner backs the input reference and which use defines the output duration?
  • Are the two lifetimes independent, identical, or ordered?
  • Does 'input: 'output express an existing fact rather than wish one into existence?
  • Would one lifetime make the contract more truthful?
  • Is a hidden T: 'a bound involved through a generic or associated type?
  • Do variance and interior mutability permit the requested shortening?
  • Is 'static being added unnecessarily to silence the error?
  • Would ownership or a scoped callback reduce the lifetime graph?

The core principle is that lifetime names identify roles, while bounds provide relationships. I fix E0623 by proving the exact outlives edge already supported by ownership, or by redesigning the interface when no such edge should exist.