Non-Lexical Lifetime Ascription


In the past the idea of lifetime ascription as a teaching tool has always been suggested with strawman syntax like 'a { ... }, implying that we would ascribe a lifetime to a curly brace block / lexical scope. Obviously, that doesn’t work for non-lexical lifetimes, which in my opinion is an extremely serious problem with the idea. Since this just came up in the comments in a way that seems important to the outcome of that RFC, now seems like a good time to talk about this. Note that I’m assuming non-lexical lifetimes are guaranteed to happen, since pretty much everything I’m about to say is a non-issue if they somehow do not happen.

In principle, I strongly believe that any feature added to the core of a programming language solely for teaching purposes is not a good idea, primarily because that makes it a feature you’ll have to “unlearn” at some point before you can start writing “real” code in that language, thereby hurting long-term learnability. This is especially true for the concrete example of lexical lifetime ascription because it simply wouldn’t work for a non-lexical lifetime at all.

I am totally okay with a feature that is primarily motivated by teaching as long as it’s not completely useless for veterans. For lifetime ascription, the only “veteran use cases” I can come up with are:

  • Documentation: Making your code very explicit about where lifetimes start and end when there’s something non-trivial going on with them. I can’t imagine this ever being an issue for lexical lifetimes, but I could easily see it being useful for sufficiently complex non-lexical ones.
  • Enforcment: Telling the compiler that a certain lifetime must start/end/include/not include a particular point in the code. From my understanding of the NLL RFC, I don’t think you’d ever need to do this to get code to compile. Maybe there are cases involing unsafe code where this could prevent undefined behavior, but I don’t know of any (can anyone more familiar with unsafe Rust confirm/deny this use case exists?) and we can already use curly braces for that purpose on lexical lifetimes.

In other words, it seems to me like lifetime ascription would be useless for veterans unless it could handle non-lexical lifetimes. And iiuc, after we get NLL most lifetimes will be at least a little bit non-lexical, so I’m not sure even novices would get much out of lexical ascription that they wouldn’t have to unlearn later.

Of course I could be totally wrong about this. Maybe it’s the very idea of a lifetime that many novices are struggling with and sticking to simple lexical lifetimes at first is a good way to ease them into that. I do not believe that is the case, because most lifetime confusion I’ve seen are not conceptual issues like “what is a lifetime?” but strictly pragmatic ones like “how do I make the borrow checker accept this code?” or “how do I make self-borrowing work?”. But I could be biased by my C++ background where everyone is expected to understand the ideas of lifetime and ownership, even if they aren’t part of the core language like they are in Rust. Still, my current opinion is that only lexical lifetime ascription is just not a very good idea, and if we’re going to talk about lifetime ascription we should at least start spitballing some syntax for ascribing/asserting non-lexical lifetimes too.

So, at the risk of starting a tangential syntax bikeshed: ISTM that we cannot hope to “draw” a whole NLL in source code, therefore whatever we do instead would have to ascribe/assert that particular points in the source code are in or not in a certain borrow/lifetime. Which would give us something like this, if we pretend the compiler reads and checks these comments:

	/* map and key are NOT borrowed */
    match map.get_mut(&key) {
        Some(value) => {
			/* map and key ARE borrowed */
        None => {
			/* map and key are NOT borrowed */
            map.insert(key, V::default());
			/* map is NOT borrowed, key is MOVED */
	/* map is NOT borrowed, key might be MOVED */