3
votes

How to get this example to compile without array copying or multiple calls to b() per iteration — b() has to perform some expensive parsing?

This is not the full code that I wrote, but it illustrates the problem I had. Here, Test is attempting to perform some kind of streaming parsing work. c() is the parsing function, it returns Some when parsing was successful. b() is a function that attempts to read more data from the stream when c() can not parse using the available data yet. The returned value is a slice into the self.v containing the parsed range.

struct Test {
    v: [u8; 10],
    index: u8,
}

impl Test {
    fn b(&mut self) {
        self.index = 1
    }

    fn c(i: &[u8]) -> Option<&[u8]> {
        Some(i)
    }

    fn a(&mut self) -> &[u8] {
        loop {
            self.b();

            match Test::c(&self.v) {
                Some(r) => return r,
                _ => continue,
            }
        }
    }
}

fn main() {
    let mut q = Test {
        v: [0; 10],
        index: 0,
    };

    q.a();
}

When compiling, it produces the following borrow checker error:

error[E0502]: cannot borrow `*self` as mutable because `self.v` is also 
borrowed as immutable
  --> <anon>:17:13
   |
17 |             self.b();
   |             ^^^^ mutable borrow occurs here
18 | 
19 |             match Test::c(&self.v) {
   |                            ------ immutable borrow occurs here
...
24 |     }
   |     - immutable borrow ends here

If I change a() to:

fn a(&mut self) -> Option<&[u8]> {
    loop {
        self.b();

        if let None = Test::c(&self.v) {
            continue
        }

        if let Some(r) = Test::c(&self.v) {
            return Some(r);
        } else {
            unreachable!();
        }
    }
}

Then it runs, but with the obvious drawback of calling the parsing function c() twice.

I kind of understand that changing self while the return value depends on it is unsafe, however, I do not understand why is the immutable borrow for self.v is still alive in the next iteration, when we attempted to call b() again.

1
looks like a compiler bug to me. I think the second version should not compile either (and in fact it doesn't if you remove the else branch) - Paolo Falabella
@PaoloFalabella, it is not a bug. Compiler can deduce that self.b() will not be called while second borrow of self.v is active, because loop unconditionally ends after this borrow is taken. If you remove else branch, then it is no longer true. - red75prime
@red75prim ah you're right, thanks! - Paolo Falabella

1 Answers

5
votes

Right now "Rustc can't "deal" with conditional borrowing returns". See this comment from Gankro on issue 21906.

It can't assign a correct lifetime to the borrow if only one execution path terminates the loop.

I can suggest this workaround, but I'm not sure it is optimal:

fn c(i: &[u8]) -> Option<(usize, usize)> {
    Some((0, i.len()))
}

fn a(&mut self) -> &[u8] {
    let parse_result;
    loop {
        self.b();

        match Test::c(&self.v) {
            Some(r) => {
                parse_result = r;
                break;
            }
            _ => {}
        }
    }
    let (start, end) = parse_result;
    &self.v[start..end]
}

You can construct result of parsing using array indexes and convert them into references outside of the loop.

Another option is to resort to unsafe to decouple lifetimes. I am not an expert in safe use of unsafe, so pay attention to comments of others.

fn a(&mut self) -> &[u8] {
    loop {
        self.b();

        match Test::c(&self.v) {
            Some(r) => return unsafe{ 
                // should be safe. It decouples lifetime of 
                // &self.v and lifetime of returned value,
                // while lifetime of returned value still
                // cannot outlive self
                ::std::slice::from_raw_parts(r.as_ptr(), r.len())
            },
            _ => continue,
        }
    }
}