Search code examples
rustlifetime

Lifetime issue using `anyhow` crate in combination with `nom` crate


I'm having an issue with lifetimes that I'm not sure how to solve, since it seems like the change I'm making should be trivial with regards to lifetimes.

Given:

use anyhow::Context;
use nom::{IResult, bytes::complete::tag};

The following code compiles:

let input = std::str::from_utf8(&output.stdout).unwrap();

let mut lines = input.lines();
let branch_line = lines.next().context("no output from `git status`")?;
let branch: IResult<&str, &str> = tag("On branch ")(branch_line);
let (branch, _) = branch.expect("failed to get name of current branch");

After changing the expect in the final line to context, the code no longer compiles:

let input = std::str::from_utf8(&output.stdout).unwrap();

let mut lines = input.lines();
let branch_line = lines.next().context("no output from `git status`")?;
let branch: IResult<&str, &str> = tag("On branch ")(branch_line);
let (branch, _) = branch.context("failed to get name of current branch")?;
error[E0597]: `output.stdout` does not live long enough
   --> src/status.rs:303:41
    |
303 |         let input = std::str::from_utf8(&output.stdout).unwrap();
    |                                         ^^^^^^^^^^^^^^ borrowed value does not live long enough
...
307 |         let branch: IResult<&str, &str> = tag("On branch ")(branch_line);
    |                     ------------------- type annotation requires that `output.stdout` is borrowed for `'static`
...
436 |     }
    |     - `output.stdout` dropped here while still borrowed

Looking at the docs for anyhow, it doesn't appear to me like it should introduce any lifetime bounds on the &output.stdout.

fn context<C>(self, context: C) -> Result<T, Error>
where
    C: Display + Send + Sync + 'static, 

Scratching my head. Still new to lifetimes.


Solution

  • The problem is the type of branch, which is IResult<&str, &str>.

    If you look at the implementation, you can see that this is an alias for IResult<&str, &str, nom::error::Error<&str>>, which again is an alias for Result<(&str, &str), nom::internal::err<nom::error::Error<&str>>>.

    This seems complicated and all, but the main point I'm trying to make is that branch is a Result type, and the Err case of it has the type nom::internal::err<nom::error::Error<&str>>. With other words, the error carries a &str.

    This is on purpose, because ownership is a big problem for nom. This is strongly related to these known problems of the current borrow checker. Nom solves this by returning ownership back through the Err type.

    That sadly means that it is incompatible with anyhow. The error types of nom are meant to be consumed by nom, or at least manually converted into something else before raised into user code.

    To explain the exact error you are getting:

    • output.stdout is a local variable
    • input, and everything else behind it, is referencing the data in output.stdout
    • The Err variant of branch is still referencing output.stdout
    • .context(), or to be more precise, the ? behind it, tries to return the Err variant out of the function, which fails because it still references output.stdout, and the reference would then outlive the data it references.

    This is not a problem for next().context(), because the None value of next() does not carry a reference to output.stdout.

    One way to fix this is to break the reference by converting the &str from the Err type to an owned String:

    use anyhow::{Context, Result};
    use nom::{bytes::complete::tag, IResult};
    
    fn main() -> Result<()> {
        let input = "aaaaaa".to_string();
    
        let mut lines = input.lines();
        let branch_line = lines.next().context("no output from `git status`")?;
        let branch: IResult<&str, &str> = tag("On branch ")(branch_line);
        let (branch, _) = branch
            .map_err(|e| e.to_owned())
            .context("failed to get name of current branch")?;
    
        Ok(())
    }