Borrow checker/lifetime error in iterator

#[derive(Debug)]
struct UvOpts {
    from: Option<String>,
    with: Box<[(String, bool)]>,
}

fn install(opts: &UvOpts) {
    let from = ["--from"]
        .into_iter()
        .chain(opts.from.iter().map(String::as_str))
        .filter(|_| opts.from.is_some());

    let mut with_enabled_commands = Vec::new();
    let mut with_disabled_commands = Vec::new();
    opts.with.iter().for_each(|(key, opt)| {
        if *opt {
            with_enabled_commands.push(key.clone());
        } else {
            with_disabled_commands.push(key.clone());
        }
    });

    let with_disabled = with_disabled_commands.join(",");
    let with_disabled_opts = ["--with", with_disabled.as_str()]
        .into_iter()
        .filter(|_| !with_disabled_commands.is_empty());

    let command = ["uv", "tool", "install"]
        .into_iter()
        .chain(from)
        .chain(with_disabled_opts);

    drop(command);
    
}

This function fails to borrow check, while saying that with_disabled does not live long enough. I'm unable to make much out of the error, and it doesn't really make sense to me. Here's a link to the playground

My question mainly is this, why does the object not live long enough? I fail to see why it's the case. What does the from field have to do with the destructor of with_disabled?

Here's the full error message:

   Compiling playground v0.0.1 (/playground)
error[E0597]: `with_disabled` does not live long enough
  --> src/main.rs:35:41
   |
34 |     let with_disabled = with_disabled_commands.join(",");
   |         ------------- binding `with_disabled` declared here
35 |     let with_disabled_opts = ["--with", with_disabled.as_str()]
   |                                         ^^^^^^^^^^^^^ borrowed value does not live long enough
...
46 | }
   | -
   | |
   | `with_disabled` dropped here while still borrowed
   | borrow might be used here, when `from` is dropped and runs the destructor for type `Filter<std::iter::Chain<std::array::IntoIter<&str, 1>, std::option::IntoIter<&str>>, {closure@src/main.rs:22:17: 22:20}>`
   |
   = note: values in a scope are dropped in the opposite order they are defined

For more information about this error, try `rustc --explain E0597`.
error: could not compile `playground` (bin "playground") due to 1 previous error

Thanks for your help.

The error about “dropped here while still borrowed” indicates that the compiler thinks that with_disabled needs to be borrowed for a lifetime that outlives the function call. Which does not make sense. I’m not sure what is going on, but it feels more like a type-inference problem than a borrow-checking problem — you've got 3 different data sources (string literals, borrowed input, borrowed locals) and it seems like it must be erroneously assuming the type of an iterator item has to have the lifetime from the borrowed input instead of a local lifetime. In particular, just re-ordering your code so that from is last succeeds:

fn install(opts: &UvOpts) {
    let mut with_enabled_commands = Vec::new();
    let mut with_disabled_commands = Vec::new();
    opts.with.iter().for_each(|(key, opt)| {
        if *opt {
            with_enabled_commands.push(key.clone());
        } else {
            with_disabled_commands.push(key.clone());
        }
    });

    let with_disabled = with_disabled_commands.join(",");
    let with_disabled_opts = ["--with", with_disabled.as_str()]
        .into_iter()
        .filter(|_| !with_disabled_commands.is_empty());

    let from = ["--from"]
        .into_iter()
        .chain(opts.from.iter().map(String::as_str))
        .filter(|_| opts.from.is_some());

    let command = ["uv", "tool", "install"]
        .into_iter()
        .chain(from)
        .chain(with_disabled_opts);

    drop(command);
}

This is probably a compiler bug, and also probably hard to fix.

1 Like

I was about to submit that fix but you explained it really well!

["--from"].into_iter() can be replaced with std::iter::once("--from")

1 Like

Thanks a lot!

For reference, here's the full code snippet I was working with, in case it gives more context

fn install_package(package: &str, opts: &UvOpts, dry_run: bool) -> Result<()> {
    let mut with_enabled_commands = Vec::new();
    let mut with_disabled_commands = Vec::new();
    opts.with.iter().for_each(|(key, opt)| {
        if *opt {
            with_enabled_commands.push(key.clone());
        } else {
            with_disabled_commands.push(key.clone());
        }
    });

    let with_enabled = with_enabled_commands.join(",");
    let with_disabled = with_disabled_commands.join(",");

    let with_enabled_opts = ["--with-executables-from", with_enabled.as_str()]
        .into_iter()
        .filter(|_| !with_enabled_commands.is_empty());

    let with_disabled_opts = ["--with", with_disabled.as_str()]
        .into_iter()
        .filter(|_| !with_disabled_commands.is_empty());


    let from = ["--from"]
        .into_iter()
        .chain(opts.from.as_deref())
        .filter(|_| opts.from.is_some());

    let git_lfs = ["--lfs"].into_iter().filter(|_| opts.with_lfs);

    let py_version = ["--python"]
        .into_iter()
        .chain(opts.py_version.as_deref())
        .filter(|_| opts.py_version.is_some());

    let command = ["uv", "tool", "install", package]
        .into_iter()
        .chain(from)
        .chain(git_lfs)
        .chain(py_version)
        .chain(with_enabled_opts)
        .chain(with_disabled_opts);

    if dry_run {
        dry_run_command(command, Perms::User)
    } else {
        run_command(command, Perms::User)
    }
}

This is the fixed version. For the signature of both dry_run_command/run_command

pub fn dry_run_command<I>(args: I, perms: Perms) -> Result<()>
where
    I: IntoIterator,
    I::Item: Into<String>,

Thanks, TIL. It'll actually reduce a lot of boilerplate in a lot of places.

The suggestion comes from clippy's. You should always try to follow them as much as possible.

Add this to the cargo.toml at the root directory

[lints.clippy]
pedantic = { level = "warn", priority = -1}
nursery = { level = "warn", priority = -1}
unwrap_unused = "deny"

I think clippy comes from the rustup toolchain.

I recommend against enabling the clippy::nursery lint group, especially to beginners. nursery is the place for lints that have significant flaws such as false positives that might give incorrect advice.

pedantic is debatable, but clippy's default settings are a better place to start, in my opinion. You don't need to configure anything, just to run cargo clippy instead of cargo check. (If you don't do that, then the configuration will have no effect.)

2 Likes