Unhelpful generics error message

I'm seeing a very unhelpful error message when dealing with generic types and lifetimes. Given the minimal code sample below (I apologize for the lack of playground link; the playground is experiencing some issues right now) the compiler gives me an error about a variable already being borrowed, even though that is not, intuitively, the reason that compilation is failing.

use std::marker::PhantomData;

struct Test<T>(PhantomData<T>);

impl<T> Test<T> {
    fn foo(&mut self, _: &T) {}
}

struct TestData<'a>(&'a [u32]);

fn main() {
    let mut a = [1, 2];
    let mut test = Test(PhantomData);
    for _ in 0..1 {
        a[0] += 1;
        let data = TestData(&a);
        test.foo(&data);
    }
}

Compilation is failing because let data = TestData(&a) is executed once per loop, so on the first loop it creates a value of type TestData<'loop1>, on the second loop a value of type TestData<'loop2>, and so on. The error given by the compiler, however, is

error[E0506]: cannot assign to `a[_]` because it is borrowed
  --> src/main.rs:16:9
   |
16 |         a[0] += 1;
   |         ^^^^^^^^^ `a[_]` is assigned to here but it was already borrowed
17 |         let data = TestData(&a);
   |                             -- `a[_]` is borrowed here
18 |         test.foo(&data);
   |         ---- borrow later used here

This presents the idea that there is some strange reverse-causal issue happening here: that an immutable borrow on line 17 is causing a mutable borrow on the line above it to fail.

While I think what's happening here does ultimately make sense — in order to make T a single coherent type, the compiler is assigning test the type Test<TestData<'all_loops>>, which means that the line 17 borrow in loop 1 is treated as still live when the line 16 borrow occurs in loop 2 — the error message is terribly unhelpful in explaining that. Ultimately, intuitively, the problem is that without lifetime expansion data is not the same type from one loop to the next.

How would I go about reporting this (Can I report this? Should I?) to see if there is a way to generate more helpful error messages in this kind of scenario?

1 Like

Rust is on github, so you search the issues looking to see if is already reported, and add an issue if you don't find anything.

I think it is worth doing. In other situations like this (or at least some of them) the error message mentions that it was borrowed in the previous loop iteration, which is what would be helpful to say here.

For comparison: The error message (and reasoning) is essentially identical to the one you get for the following code.

fn main() {
    let mut a = [1, 2];
    let mut test = vec![];
    for _ in 0..1 {
        a[0] += 1;
        let data = &a;
        test.push(data);
    }
}
error[E0506]: cannot assign to `a[_]` because it is borrowed
 --> src/main.rs:5:9
  |
5 |         a[0] += 1;
  |         ^^^^^^^^^ `a[_]` is assigned to here but it was already borrowed
6 |         let data = &a;
  |                    -- `a[_]` is borrowed here
7 |         test.push(data);
  |         ---- borrow later used here

This version of the example seems like an important point of comparison to me, as any change/improvement to the error message should apply here equally, and also there’s no possible argument to be had about whether or not the design of Test<T>’s API could be changed to help avoid the issue.


Now, this comparison – at least IMO – clearly raises the question of whether we would actually prefer some sort of “data is not the right type” error message here. In this case, where it isn’t just a PhantomData that connects the lifetimes, it seems like a more clear proper borrowing issue. But the difference between PhantomData or Vec is not really visible to the borrow checker.

That being said, I too came across instances where I felt about borrow-checking errors that they were almost more of a simply type mismatch error; anyways…

…if I had to describe the issues with the current error messages, I’d say.

  1. to someone not used to borrow-checking errors, the order can be confusing. This is a common occurrence with loops. Some other borrow-checking-error patterns already have special error messages for loops [1], this one doesn’t. It’s not super hard to understand the right order, especially if you’ve seen many general borrow-checking errors, because the phrasing of the messages implies that first a[0] is borrowed, then it’s accessed mutably, and finally the initial borrow is later accessed. [The phrasing can get even more unambiguous if you remove the compiler’s ability to track the a[_] position, and borrow the whole of a instead[2]]

  2. the error message does not explain the connection of the borrow of a[_] with the variable test. It simply implies that … “well obviously”… the usage of test in test.push(…) constitutes an access to the first borrow of a. This is often easy to understand; e.g. in the version with a Vec we can intuitively understand that … “well, the borrow has been pushed into that Vec” … so of course test.…whatever… will have access to this borrow.

    However, with your .foo method, a PhantomData, and then also the fact that foo only takes a short-lived reference to T, it can be a lot more subtle. Of course whether it’s _: T or _: &T is irrelevant from a borrow checking perspective – e.g. the latter could easily be held for longer with a call to Clone. However, especially the fact that the &T reference has an additional lifetime of its own can easily distract from how the type parameter T, too, can contain a borrow, and for much longer than the &T reference lives, too!


  1. for example

    fn main() {
        let mut a = [1, 2];
        let mut test = vec![];
        for _ in 0..1 {
            let data = &mut a;
            test.push(data);
        }
    }
    
    error[E0499]: cannot borrow `a` as mutable more than once at a time
    --> src/main.rs:5:20
    |
    5 |         let data = &mut a;
    |                      ^^^^^^ `a` was mutably borrowed here in the previous iteration of the loop
    6 |         test.push(data);
    |           ---- first borrow used here, in later iteration of loop
    
    ↩︎
  2. fn main() {
        let mut a = [1, 2];
        let mut test = vec![];
        for _ in 0..1 {
            (&mut a)[0] += 1;
            let data = &a;
            test.push(data);
        }
    }
    
    error[E0502]: cannot borrow `a` as mutable because it is also borrowed as immutable
    --> src/main.rs:5:9
    |
    5 |         (&mut a)[0] += 1;
    |           ^^^^^^^^ mutable borrow occurs here
    6 |         let data = &a;
    |                      -- immutable borrow occurs here
    7 |         test.push(data);
    |           ---- immutable borrow later used here
    

    …actually, now that I’m looking at this again, it might be just as confusing, as the first message mutable borrow occurs here is actually less explicit about how the issue is that that mutable borrow happens while the other, immutable borrow, already exists. ↩︎

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.