rust-lang / rust-lang/rust

Lifetime issues caused by implicitly choosing the wrong implementation of Borrow are incomprehensible

Open
#136,793 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-diagnostics T-compiler
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

Code
use std::collections::BTreeSet;

pub fn find_static<'a>(map: &BTreeSet<&'static str>, key: &'a str) -> &'static str {
    *map.get(&key).unwrap()
}
Current output
error: lifetime may not live long enough
 --> src/lib.rs:4:5
  |
3 | pub fn find_static<'a>(map: &BTreeSet<&'static str>, key: &'a str) -> &'static str {
  |                    -- lifetime `'a` defined here
4 |     *map.get(&key).unwrap()
  |     ^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'a` must outlive `'static`
Desired output
error: lifetime may not live long enough
 --> src/lib.rs:4:5
  |
3 | pub fn find_static<'a>(map: &BTreeSet<&'static str>, key: &'a str) -> &'static str {
  |                    -- lifetime `'a` defined here
4 |     *map.get(&key).unwrap()
  |     ^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'a` must outlive `'static`
note: `'static` in the type of `map` narrowed to `'a` to match `key`
note: lifetimes must match due to the implementation of `Borrow` being used
 --> rustc-src/std/blah/borrow.rs:12345
   impl<T> Borrow<T> for T { ... }
   ^^^^^^^^^^^^^^^^^^^^^^^
help: consider removing the borrow here
  |
4 |     *map.get(&key).unwrap()
  |              -
  |             ( key)
Rationale and extra context

This is a very confusing case that definitely needs more attention in the diagnostics in order to be understandable by a typical Rust developer. After much head-scratching, I have arrived at the following explanation of why this works like that:

  • when doing get(key), the trait bounds require &'static str: Borrow<str>, which resolves through impl<T> Borrow<T> for &T and all is good with the world
  • when doing get(&key), the trait bounds require &'static str: Borrow<&'a str>, which resolves through impl<T> Borrow<T> for T and unifies 'static with 'a, which means that the compiler uses variance to shrink the 'static in &BTreeSet<&'static str> to &BTreeSet<&'a str>
Other cases

If the key is a lifetime-parametrized struct, there is no easy way of achieving the same thing:

use std::collections::BTreeMap;

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Meow<'a> {
    pub name: &'a str,
    pub index: usize,
}

pub fn launder_lifetime<'a>(map: &BTreeMap<Meow<'static>, usize>, key: Meow<'a>) -> Meow<'static> {
    // same error as above
    *map.get_key_value(&key).unwrap().0
}

The best way of making this compile that I know of involves this monstrosity:

use std::borrow::Borrow;
use std::collections::BTreeMap;

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Meow<'a> {
    pub name: &'a str,
    pub index: usize,
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct UglyWorkaround<'a>(Meow<'a>);

impl<'a, 'b: 'a> Borrow<Meow<'a>> for UglyWorkaround<'b> {
    fn borrow(&self) -> &Meow<'a> {
        &self.0
    }
}

pub fn launder_lifetime<'a>(map: &BTreeMap<UglyWorkaround<'static>, usize>, key: Meow<'a>) -> Meow<'static> {
    map.get_key_value(&key).unwrap().0.0
}

Guiding the user towards that would probably have to involve an example in the docs...

Rust Version
rustc 1.86.0-nightly (43ca9d18e 2025-02-08)
binary: rustc
commit-hash: 43ca9d18e333797f0aa3b525501a7cec8d61a96b
commit-date: 2025-02-08
host: x86_64-unknown-linux-gnu
release: 1.86.0-nightly
LLVM version: 19.1.7

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the provided BTreeSet and BTreeMap examples with the stated Rust nightly version, then trace the compiler diagnostic path for the lifetime error and the selected Borrow implementation. Compare the current output with the requested notes and help; done means the diagnostics explain the implicit Borrow choice and offer an appropriate correction, with regression coverage for the examples.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.