Private lifetime inference

[Pre-RFC] Lifetime inference for non-public items

Summary

Allow the compiler to infer lifetime parameters for all items (functions, structs, enums, type aliases, impl blocks) that are not pub at the crate boundary. Public API continues to require explicit annotations as today.

The problem

Private code pays the same lifetime annotation tax as public API, for zero benefit to crate consumers.

Functions โ€” the classic case where elision is insufficient:

// Today: must annotate manually, even though it's private
fn find_best<'a>(primary: &'a Data, fallback: &'a Data) -> &'a Summary {
    if primary.is_valid() { &primary.summary } else { &fallback.summary }
}

Structs โ€” this is where the pain really compounds:

// Private struct โ€” 'a is obvious from the fields
struct ParseState<'a> {
    input: &'a str,
    tokens: &'a [Token],
    current: &'a Token,
}

// Now EVERY function must propagate 'a:
fn parse_expr<'a>(state: &mut ParseState<'a>) -> Expr<'a> { ... }
fn parse_binary<'a>(state: &mut ParseState<'a>) -> Expr<'a> { ... }
fn parse_atom<'a>(state: &mut ParseState<'a>) -> Expr<'a> { ... }
// ... repeat 20 more times

Change one lifetime in ParseState โ†’ update 20+ function signatures. The compiler knows the right answer. Why am I doing this by hand?

Enums, type aliases, impl blocks โ€” same story:

enum CacheEntry<'a> { Hit(&'a Response), Miss }
type Pair<'a> = (&'a str, &'a str);
impl<'a> ParseState<'a> { fn advance(&mut self) -> &'a Token { ... } }

The proposal

For items not reachable from outside the crate, the compiler infers lifetimes:

// All private โ€” no lifetime annotations needed:

struct ParseState {
    input: &str,
    tokens: &[Token],
    current: &Token,
}

impl ParseState {
    fn advance(&mut self) -> &Token { ... }
}

fn parse_expr(state: &mut ParseState) -> Expr { ... }

enum CacheEntry { Hit(&Response), Miss }

For pub items at crate boundary โ€” everything stays as today. When you change a private item to pub, the compiler tells you to add annotations and suggests the correct ones.

Why this makes sense

  1. Closures already do this. |x: &str, y: &str| -> &str { x } infers lifetimes from the body. Private named items are equally invisible to crate consumers.

  2. Precedent in Rust. RFC 2093 added inference of T: 'a bounds on structs. Variance inference is a whole-crate fixed-point analysis that works silently. This is the same philosophy.

  3. Precedent in other languages. Kotlin and Swift require explicit types on public API but infer for private code. The visibility-based boundary works.

  4. The crate boundary is already special. Type privacy (RFC 2145), impl Trait opacity, auto-trait leakage, orphan rules โ€” all use the same boundary.

Visibility rule

Visibility Inference?
private, pub(self), pub(super), pub(crate) :white_check_mark: Yes
pub at crate boundary :cross_mark: No
pub trait method declarations :cross_mark: No

Open questions

  • Struct inference strategy: infer lifetimes purely from field types (simple, local) or also from usage sites (precise, whole-crate)? Variance inference already does whole-crate analysis, so there's precedent.
  • Should pub(crate) be included? I think yes โ€” it's still internal to the crate. But open to discussion.
  • Private traits: inferring method lifetimes from implementations is the most complex part. Worth deferring to a follow-up?
  • Phased approach: start with functions only? Or include structs/enums from the start? The struct case is where the biggest ergonomic win is, so I'd argue for including it.

What I'm looking for

Feedback on:

  • Is this worth pursuing as an RFC?
  • What scope makes sense for a first proposal (functions only vs. all items)?
  • Any technical blockers I'm missing?
  • Pointers to prior discussions on this topic

I have a full RFC draft ready if there's interest.

It sounds to me like your biggest problem is having to update a bunch of item signatures whenever you change the lifetime annotations on a type. I think it'd be better to resolve that pain point by adding better tooling (could rust analyzer get an action to add/remove a lifetime annotation for an item?), not by changing the actual language itself.

I like having item signatures be fully explicit for reading existing code, even when it's not a public API it helps me quickly understand what the code does.

5 Likes

But it is busywork, it does not improve safety of code, that is why it woulds be nice to have such life-time elision

Does it?

error: lifetime may not live long enough
 --> src/main.rs:2:34
  |
2 |     |x: &str, y: &str| -> &str { x };
  |         -                 -      ^ returning this value requires that `'1` must outlive `'2`
  |         |                 |
  |         |                 let's call the lifetime of this reference `'2`
  |         let's call the lifetime of this reference `'1`

Playground

5 Likes

A couple other nits:

Opaque types don't use a crate boundary, and auto-trait leakage has no boundary.

Privacy isn't as straightforward as what the pub(..) annotation says thanks to re-exports.

3 Likes

Valid point

Hence my suggestion to go at the busywork of it more directly

It does not solve issue of cascading changes when life-time change

I just want to bring this up as a restricted version of this proposal.

The main problem is circular dependency. You may need item foo's definition to infer bar's definition and vice versa at the same time. This will results a complex constrain solving, and that may not have an unique inference result.

Compiler anyway doing this, do not matter if compiler infer life-time and verify or check if life-time provided by user correct and then verify them

This is false, but I don't have enough time/motivation to explain in detail. Simply put, inferring a valid lifetime constraints is much more complex than checking user annotated lifetimes. (Well, obviously assuming you don't want the compiler reject all non-trivial code).

1 Like

this is very incorrect. the compiler does not know, nor can it know. what the "right" lifetimes should be, as they depend on many factors the compiler cannot consider.

many newer users already suffer from the current inference rules(in particular looking at elided_lifetimes_in_paths) when trying to debug lifetimes, they do not need more confusion.

this is a great simple example of somewhere where the compiler cannot infer what the right lifetimes should be. it could very well be, and often is, find_best<'a>(primary: &'a Data, fallback: &Data) -> &'a Summary , or find_best<'a>(primary: &Data, fallback: &'a Data) -> &'a Summary .

i encourage you to read this rust-blog/posts/common-rust-lifetime-misconceptions.md at master ยท pretzelhammer/rust-blog ยท GitHub .

TBH, getting lifetime bounds right is rarely the noisy part. I think it'd be entirely fine to solve that part like we solve -> _: with a structured suggestion inferred from the body.

Then I agree with the mention of a macro fn: then it could potentially even just be

macro fn find_best(primary, fallback) {
    if primary.is_valid() { &primary.summary } else { &fallback.summary }
}

where not only do I not need to put in the lifetimes but I can even leave off the types and have it just happen in the context of the caller instead.

Which would, among other things, be really really useful for math where writing out all the num_traits bounds is particularly annoying for a small helper.

7 Likes

Why compiler does not know ?? Compiler knows graph of objects (AST) and it could traverse path of variables. Of course it is sometimes problematic to do, but in obvious cases it could simplify life of developers a lot !!

Yes, it was a mistake in Pre-RFC, but it even prove the this feature is needed, even closure could not bypass requirements for explicit life-times !!