Possibly misleading error message regarding generic lifetime bound

While learning about generic lifetime bounds, I created the following example:

fn main() {
   let vec = [10, 11];
   let a = A (&vec);
   println!("{:?}", a);
   let r = &a;
   // following line 7 is the cause of the error, but compiler complains about line 3
   let b = A(r);
   println!("{:?}", b);
}

#[derive(Debug)]
struct A<'a, T: 'static>(&'a T);

As expected, this fails to compile because r in line 5 is a temporary reference which does not satisfy the required 'static lifetime bound for T.

But the error message reads (using rustc 1.72.0):

error[E0597]: `vec` does not live long enough
  --> example.rs:3:15
   |
2  |    let vec = [10, 11];
   |        --- binding `vec` declared here
3  |    let a = A (&vec);
   |            ---^^^^-
   |            |  |
   |            |  borrowed value does not live long enough
   |            assignment requires that `vec` is borrowed for `'static`
...
10 | }
   | - `vec` dropped here while still borrowed

The point is that it complains about line 3, which is perfectly legal. If I understand correctly &vec satisfies 'static because it does not contain any reference.

Whereas line 7 is clearly the culprit, it is not mentioned in the error message at all. If line 7 is removed as in

   //let b = A(r);
   println!("{:?}", r);

the example compiles and runs just fine. This confirms that line 3 must be indeed correct, despite the error message.

Could this be a bug in he compiler's error reporting or do I misunderstand something?

1 Like

It's reference by itself, how can it satisfy 'static if your vec is not 'static?

Yet one can fix line 2, like complier said, and program would work:

pub fn main() {
   static vec: [i32; 2] = [10, 11];
   let a = A (&vec);
   println!("{:?}", a);
   let r = &a;
   // following line 7 is the cause of the error, but compiler complains about line 3
   let b = A(r);
   println!("{:?}", b);
}

#[derive(Debug)]
struct A<'a, T: 'static>(&'a T);

Maybe it’s a bug, it might also not really be a bug; either way, there’s definitely some ways the diagnostics could be improved.

It seems like one way to think about how the compiler might approach this code is that the line b = A(r) makes the compiler infer the type of a to be A<'static, _>. Then with that information, assigning to a in line 3 is the problem. Notable type inference doesn’t have to go all forwards.

Also, the observation that changing or removing line 7 makes the code compile does not mean that line 7 is “the culprit”. In fact there need not be one specific culprit, there can be multiple that only together create a problem. The problem here can be “fixed” in line 3 as well, e.g. is you create a 'static reference (via constant static promotion) via

-  let vec = [10, 11];
-  let a = A (&vec);
+  let a = A (&[10, 11]);

and there you have the problem gone, without touching line 7.

Nonetheless, as mentioned there is room for improvement in the error message. The explanation “assignment requires that vec is borrowed for 'static” is too vague; you’ll immediately wonder “why?” and it would be nice if the compiler pointed out the call to A’s constructor, in line 7, where the relevant explicit : 'static bound actually got introduced.

On the other hand, it’s not exactly a particularly uncommon thing that borrow checking errors are lacking detail on the exact way how two different lifetimes turned out related. E.g. quite often “borrow later used here”-style messages point to places that are not directly using the borrowed variable in question, but commonly other variables that are only related in lifetime, e.g. a Vec that holds references of the same lifetime, or the result of some method that relates the lifetimes.

I wonder why this is this way. On one hand, maybe providing more information might commonly be a bit overkill / lead to very long error messages explaining fairly obvious stuff. On the other hand, explicit error messages are usually a good thing, so maybe that’s not it – maybe the way the borrow checker operates is sufficiently far removed from the code that it’s plainly hard to map back the result “check didn’t pass” to a human-understandable explanation that relates to the original source-code in question. Maybe it’s just hard to write such diagnostics, or maybe no-one has come around to improve them more? Maybe there’s even performance implications in doing more bookkeeping during borrow-checking, for better error messages?

I’d be interesting to experiment some more here what versions of the code produce what results. E.g. a straighforward

fn f<T: 'static>(_: T) {}

fn main() {
   let x = 1;
   let y = &x;
   f(y);
}

does point to the f(y) call in the error

error[E0597]: `x` does not live long enough
 --> src/main.rs:5:12
  |
4 |    let x = 1;
  |        - binding `x` declared here
5 |    let y = &x;
  |            ^^ borrowed value does not live long enough
6 |    f(y);
  |    ---- argument requires that `x` is borrowed for `'static`
7 | }
  | - `x` dropped here while still borrowed

I haven’t tested more variations yet.

Well, the entirety of your function body has to pass borrow check, and it didn't. It's basically a constraint satisfaction problem. The compiler then has to backtrack from some borrow check violation to try and give a reasonable error message.

For example, getting rid of line 7 (and 8) makes the error go away. But so does this change to lines 2-3:

-   let vec = [10, 11];
-   let a = A (&vec);
+   static VEC: Vec<i32> = Vec::new();
+   let a = A (&VEC);

So it's not a bug really, it's a diagnostic deficiency at worst. The question becomes, is the error reasonable enough? Probably it could be improved by pointing out the T: 'static bound on A as the source of the lifetime requirement.

Not even. One may go and fix line 2, which compiler names the culprit without touching like 3 or line 7. And program would work.

Precisely that. In place where compiler detects the issue it just have two incompatible constraints which come from dozen of other places in the program.

It backtracks to one place where change to constraint may fix the program but it's very hard to predict which one is the best.

Here’s a similar/simplified situation

fn main() {
    let n = 1;

    let r = identity(&n);
    
    need_static(r);
}

fn identity<T>(x: T) -> T {
    x
}

fn need_static<T: 'static>(_: T) {}
error[E0597]: `n` does not live long enough
 --> src/main.rs:4:22
  |
2 |     let n = 1;
  |         - binding `n` declared here
3 |
4 |     let r = identity(&n);
  |             ---------^^-
  |             |        |
  |             |        borrowed value does not live long enough
  |             argument requires that `n` is borrowed for `'static`
...
7 | }
  | - `n` dropped here while still borrowed

Somehow in this situation, the diagnostics feels even worse to me. (I know it’s not actually that much of a different situation, and all I’ve said in my answer above still applies here. It’s just my “feeling”; perhaps because I understand an identity function like this as particularly harmless, and don’t want the compiler to “blame” it.)

I do believe that the error message is sufficiently bad that one should definitely open an issue, if there isn’t one already :slight_smile:

It would only be constructive if you would constructively explain what exactly is wrong with error message and how precisely you want to improve it.

Right now compiler says: you binding n is too short-living, go and fix that. And if you go and fix precisely what compiler complains about program works:

pub fn main() {
    static n: i32 = 1;

    let r = identity(&n);
    
    need_static(r);
}

fn identity<T>(x: T) -> T {
    x
}

fn need_static<T: 'static>(_: T) {}

Yes, it does not “feel” like a good error message, it's not “sane” solution to the problem, but compiler doesn't have common sense, it couldn't do “sane”. We need some objectively measurable criteria to push compiler into different direction of lifetime conflict resolution.

That feel close to some actual rule which can actually be added to the compiler. It's still hard to say what's wrong with diagnosis but, indeed, the fact that function without any constraints is shown as culprit is a bit baffling.

The good thing about issues is that one person can notice the issue and first report it, then other can further discuss the issue, and maybe then someone can point out more precisely what can be improved.

Diagnostics are meant to help humans understand errors. If one human finds a diagnostic not sufficiently useful, (and others agree) than that is an issue.

I don’t think it’s a good mentality to suggest that you shouldn’t open issues on the Rust repo unless you can also explain how precisely the rust compiler can be changed to improve the situation. (You probably didn’t mean it in this extreme way either, but your words can certainly be interpreted that way.) The extreme version of this interpretation would be “don’t raise an issue unless you also provide a PR that closes it”.


On the note of “what exactly” is wrong with the error message: If we assume that there’s multiple places to possibly “blame” (and I don’t mean the place the borrow is created, or the thing that is borrowed… you demonstrate that those can influence the situation, too, but mind that the compiler is already pointing out the variable being borrowed and where the borrow is created, and even where the variable is dropped) i.e. in my mind there is both the call to identity and to need_static that is relevant.

I wouldn’t even be against just pointing to both. After all, we have other diagnostics that point out a whole chain of reasoning. E.g. why a certain trait implementation or auto trait implementation does or doesn’t exist, and is or isn’t required.

If there is a good reason to aim for limiting ourselves to pointing out only one place, then I feel like identity has a “propagating” sort of role of “requiring the borrow to be 'static” whereas need_static’s signature is the “true source” of that requirement. It definitely seems useful, in my mind, to point to the relevant call in the code where the function with the literal 'static bound that caused the 'static requirement in the first place was called.

This reasoning may very well be somewhat unique to “'static”, too, as it’s such a clearly named lifetime that can explicitly appear in signatures. For other lifetimes, I just haven’t thought about it much yet though – perhaps one would first need to come up with cases of “seemingly problematic” error messages anyways.

On that note, the following code examples are an interesting comparison, the latter case with a non-'static (but named) lifetime gives a “better” error message (i.e. points to a different place); and asking “why in the world are these any different?” alone seems to me like another convincing argument to open an issue (if it doesn’t exist already), too:

fn demo<'a>() {
    let n = 1;

    let r = identity(&n);

    need_lifetime::<'static>(r);
}

fn identity<T>(x: T) -> T {
    x
}

fn need_lifetime<'a, T: 'a>(_: T) {}
error[E0597]: `n` does not live long enough
 --> src/lib.rs:4:22
  |
2 |     let n = 1;
  |         - binding `n` declared here
3 |
4 |     let r = identity(&n);
  |             ---------^^-
  |             |        |
  |             |        borrowed value does not live long enough
  |             argument requires that `n` is borrowed for `'static`
...
7 | }
  | - `n` dropped here while still borrowed
fn demo<'a>() {
    let n = 1;

    let r = identity(&n);

    need_lifetime::<'a>(r);
}

fn identity<T>(x: T) -> T {
    x
}

fn need_lifetime<'a, T: 'a>(_: T) {}
error[E0597]: `n` does not live long enough
 --> src/lib.rs:4:22
  |
1 | fn demo<'a>() {
  |         -- lifetime `'a` defined here
2 |     let n = 1;
  |         - binding `n` declared here
3 |
4 |     let r = identity(&n);
  |                      ^^ borrowed value does not live long enough
5 |
6 |     need_lifetime::<'a>(r);
  |     ---------------------- argument requires that `n` is borrowed for `'a`
7 | }
  | - `n` dropped here while still borrowed

1 Like

Thanks for the quick reply. Satisfying 'static refers to the lifetime boundary set on the generic type T itself (line 12). The reference to T has the assigned lifetime a' .
According to the Rust book (second edition) chapter 19.2
[Advanced Lifetimes - The Rust Programming Language] it says:

... Because 'static means the reference must live as long as the entire program, a type that contains no references meets the criteria of all references living as long as the entire program (because there are no references). For the borrow checker concerned about references living long enough, there is no real distinction between a type that has no references and a type that has references that live forever...

Thus 'static is about references inside T of which there are obviously none for case a where T = [u32; 2]. However, there is a non-'static reference in the case of b where T = A<[u32; 2]> where struct A holds a reference to the local vec.

Sure.

Who told you your type is T = [u32; 2]?

In line 7 it's A<'something, [i32; 2]> and because you quite explicitly told the compiler that you want that 'something to be 'static it goes to where that 'something is defined and tried to verify that it's equal to 'static.

And since your type T is created in line 3 now you need vec to be 'static for 'something to be 'static. And it's not 'static. Thus we have an error. In line 2 where it may be fixed easily.

It helps to add explicit types everywhere:

pub fn main() {
   let vec: [u32; 2] = [10, 11];
   let a: A<'_, [u32; 2]> = A (&vec);
   println!("{:?}", a);
   let r: &'_ A<'_, [u32; 2]> = &a;
   // following line 7 is the cause of the error, but compiler complains about line 3
   let b: A<'_, A<'_, [u32; 2]>> = A(r);
   println!("{:?}", b);
}

Yes, line 7 makes compiler emit error, but that doesn't mean error is in line 7. Compiler's take is that you have short-lived type A<'something, [u32; 2]> when you asked for A<'static, [u32; 2]> is just as valid as your interpretation that line is in error 7.

If you change type type in line three from A<&'something, [u32; 2]> to A<'static, [u32; 2]> then program compiles as I have shown.

Why do you say that one solution is worse than the other? In your program in line 3 types are different (A<'something, [u32; 2]> on the right, A<'static, [u32; 2]> on the left, because of type inference) and something must be changed.

Yes compiler offers one solution, but that doesn't mean it's the only solution. Compiler is just a compiler, it couldn't write programs for you.

Consider similar example without lifetimes or generics:

pub fn main() {
   let x = -1;
   println!("{x:?}");
   let y = x;
   println!("{y:?}");
   foo(y)
}

fn foo(_: u32) {
}

Here the compiler's complaint it thus:

error[E0277]: the trait bound `u32: Neg` is not satisfied
 --> <source>:2:12
  |
2 |    let x = -1;
  |            ^^ the trait `Neg` is not implemented for `u32`
  |
  = help: the following other types implement trait `Neg`:
            isize
            i8
            i16
            i32
            i64
            i128
            f32
            f64
          and 8 others

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
Compiler returned: 1

Error is in line 2 because you are trying to assign negative number to u32. But said variable has type u32 because said type propagates back from function foo definition to line 2 where x is defined!

In your example situation is the same, only lifetime propagates back from constraint definition in your struct A function back to line 2 where vec is defined.

Where in my example type u32 propagated back to assignment x = -1 and caused error there.

Thanks to all for the discussion. To summarize what I learned from it:

The problem here is that I might had the wrong assumption that the type (and required lifetimes) of a in line 3 is infererred by the compiler considering only the fist two lines of main(). But the compiler may have a broader view and cannot predict what I had in mind. I do not fully grasp the details yet, but I can accept.

That said, there is still room to improve error diagnostics even if the Rust compiler's error reporting facilities are already great. Kudos to the Rust team :slightly_smiling_face:

I will not raise an issue about that, because this special case seems only being a part of the bigger scope where some corner cases might still need improvements. This might already be considered. If someone of the more experienced users think that it is indeed worth opening a dedicated issue, please feel free to do so.

I’ve taken a look into it and there’s existing issues…

e.g. this issue

and there’s even this PR that aims to fix it

I could also find this issue

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.