RFA-642 · Case file with fixtures · Case 614 of 694 · Compiler evidence
Late-Bound Lifetimes Are Chosen at the Call Site
A function that works for any borrow has a higher-ranked lifetime contract. Let each invocation choose the lifetime; use for<'a> when naming the function-pointer relationship.
- 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
- A universally borrow-polymorphic function was mistaken for an item whose lifetime is selected once with turbofish syntax.
- First discriminating check
- Remove the explicit lifetime and let each call choose, using for<'a> when the callable relationship must be named.
fn identity<'a>(&'a str) -> &'a str works with a short local borrow, a static string, and many lifetimes between them. Its lifetime is selected for each call from the argument. The failing fixture tries to preselect 'static on the function item and receives E0794.
Universal use is stronger than one chosen lifetime
The function can be understood as “for every lifetime 'a, accept &'a str and return a borrow with that same 'a.” This is universal quantification, written explicitly in a function pointer as for<'a> fn(&'a str) -> &'a str.
The official E0794 explanation calls this parameter late-bound and says it cannot be supplied explicitly. The repaired fixture assigns the function to the higher-ranked pointer type and lets each call choose.
Making it only 'static would be less reusable, not more. It would reject normal local strings even though the body does not need long-lived storage.
Late and early binding depend on use
A lifetime appearing in a function argument and not constrained in certain generic bounds is commonly late-bound. An early-bound lifetime participates in selecting a particular function instance alongside type or const parameters.
The spelling in the declaration looks similar, so I do not classify by visual position alone. The official E0794 page gives an example containing both kinds and shows which generic arguments can be supplied.
When an error is dense, I reduce it to the relationships: which borrow enters, which output borrows from it, and whether a where-clause or self type makes lifetime choice part of the item identity.
Higher-ranked bounds appear in callback APIs
A callback for<'a> Fn(&'a Record) must accept a record borrowed for any lifetime chosen by the caller. This differs from Fn(&'fixed Record), where one lifetime was chosen outside the call.
The Reference explains higher-ranked trait bounds. The Rustonomicon’s HRTB chapter connects this syntax to closures over borrowed data.
I meet this in visitors, parsers, and APIs lending temporary views. The callback cannot retain an arbitrary short borrow unless its output and storage contract say how. This is exactly the protection these bounds provide.
Do not use static as a lifetime solvent
Adding 'static often appears to reduce compiler complexity, but it can change ownership architecture. It may force cloning request data, leaking allocations, or rejecting useful borrowed inputs.
'static on a type bound means the value contains no non-static references; it does not mean the value itself lives forever. On a reference, it means the referent is valid for the entire program. I keep those meanings separate.
For spawned tasks, owning data may genuinely be required because the task can outlive its caller. That is a different boundary from invoking a synchronous higher-ranked callback during one borrow.
Function items, pointers, and closures differ
A named function item has a unique zero-sized type and can coerce to an appropriate function pointer. A non-capturing closure may also coerce. Capturing closures carry environment state and implement one or more Fn traits according to capture use.
I state the weakest interface needed. A generic F: for<'a> Fn(&'a str) can inline different closure types; a for<'a> fn(...) pointer accepts only function-pointer-compatible callables. A boxed dyn callback adds ownership and dynamic dispatch.
Compile tests use multiple local scopes to prove the callback accepts truly short borrows. Testing only string literals can accidentally make every example static and hide an over-constrained design.
My E0794 checklist
- Which explicit lifetime argument targets a late-bound function lifetime?
- Does the input/output relation mean each call should choose its own borrow duration?
- Can the explicit turbofish lifetime simply be removed?
- When naming the callable, should its type use
for<'a>? - Did a where-clause make another lifetime early-bound?
- Is
'staticbeing added for convenience instead of a real ownership reason? - Does the API accept functions, capturing closures, or dynamically stored callbacks?
- Do tests call it with non-static locals from several scopes?
The core principle is that a universally borrow-polymorphic function is not instantiated once with a chosen lifetime. E0794 preserves call-site selection. I express the relationship with higher-ranked syntax when it must be named and let actual arguments determine each borrow.