Skip to content

fixed-by-polonius loops/functions have very bad diagnostics #125217

Open
@jyn514

Description

@jyn514

Code

struct Command(Vec<String>);
impl Command {
    fn find_subcommand_mut(&mut self, _name: &str) -> Option<&mut Self> {
        None
    }
}

fn foo(app: &mut Command) {
    let alias_definition = vec!["submodule", "status"];
    let mut cmd = app;
    for arg in alias_definition {
        if let Some(next) = cmd.find_subcommand_mut(arg) {
            cmd = next;
        } else {
            break;
        }
    }
    dbg!(&cmd.0);
}

Current output

error[E0502]: cannot borrow `cmd.0` as immutable because it is also borrowed as mutable
  --> src/lib.rs:18:10
   |
12 |         if let Some(next) = cmd.find_subcommand_mut(arg) {
   |                             --- mutable borrow occurs here
...
18 |     dbg!(&cmd.0);
   |          ^^^^^^
   |          |
   |          immutable borrow occurs here
   |          mutable borrow later used here

Desired output

"use -Zpolonius lol"

Rationale and extra context

"immutable borrow used here" pointing at the same span as "mutable borrow used here" is very unhelpful. the only case this could possibly make sense is when the code is in a loop, which this is not.

i originally ran into this problem inside of an (unrelated) loop, making the diagnostic even more confusing.

Other cases

removing break makes the error a little better, but it still would be nice to say "this is a limitation of lifetimes" instead of "your code is broken"

fn foo(app: &mut Command) {
    let alias_definition = vec!["submodule", "status"];
    let mut cmd = app;
    for arg in alias_definition {
        if let Some(next) = cmd.find_subcommand_mut(arg) {
            cmd = next;
        }
    }
}
error[E0499]: cannot borrow `*cmd` as mutable more than once at a time
  --> src/lib.rs:12:29
   |
12 |         if let Some(next) = cmd.find_subcommand_mut(arg) {
   |                             ^^^
   |                             |
   |                             `*cmd` was mutably borrowed here in the previous iteration of the loop
   |                             first borrow used here, in later iteration of loop

the recursive case is also pretty confusing, although it looks more like a "traditional" polonius error so maybe it's easier to recognize

use clap::Command; // 4.5.4

fn find_nested_cmd<'a, 'b, 'c>(cmd: &'a mut Command, query: &'b [&'c str]) -> (&'a mut Command, &'b [&'c str]) {
    // let next = query.first().and_then(|arg| cmd.find_subcommand_mut(arg));
    if let Some(arg) = query.first() {
        if let Some(next) = cmd.find_subcommand_mut(arg) {
            return find_nested_cmd(next, &query[1..]);
        }
    }
    (cmd, query)
}

fn foo(app: &mut Command) {
    let alias_definition = vec!["submodule", "status"];
    let (subcmd, args) = find_nested_cmd(app, &alias_definition);
    dbg!(subcmd.get_bin_name());
}
error[E0499]: cannot borrow `*cmd` as mutable more than once at a time
  --> src/lib.rs:10:6
   |
3  | fn find_nested_cmd<'a, 'b, 'c>(cmd: &'a mut Command, query: &'b [&'c str]) -> (&'a mut Command, &'b [&'c str]) {
   |                    -- lifetime `'a` defined here
...
6  |         if let Some(next) = cmd.find_subcommand_mut(arg) {
   |                             --- first mutable borrow occurs here
7  |             return find_nested_cmd(next, &query[1..]);
   |                    ---------------------------------- returning this value requires that `*cmd` is borrowed for `'a`
...
10 |     (cmd, query)
   |      ^^^ second mutable borrow occurs here

another workaround is to add an extra unwrap() so it doesn't look to the compiler like the lifetimes are related:

    for arg in alias_definition {
        if let Some(next) = cmd.find_subcommand_mut(arg) {
            cmd = cmd.find_subcommand_mut(arg).unwrap();
        }
    }

Rust Version

rustc 1.80.0-nightly (8c127df75 2024-05-16)
binary: rustc
commit-hash: 8c127df75fde3d5ad8ef9af664962a7676288b52
commit-date: 2024-05-16
host: x86_64-unknown-linux-gnu
release: 1.80.0-nightly
LLVM version: 18.1.4

Anything else?

@rustbot label fixed-by-polonius

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-borrow-checkerArea: The borrow checkerA-diagnosticsArea: Messages for errors, warnings, and lintsD-confusingDiagnostics: Confusing error or lint that should be reworked.D-terseDiagnostics: An error or lint that doesn't give enough information about the problem at hand.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.fixed-by-poloniusCompiling with `-Zpolonius` fixes this issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions