
Rust RFC 2094: non-lexical lifetimes - JoshTriplett
https://github.com/rust-lang/rfcs/pull/2094
======
the_common_man
Previous discussion -
[https://news.ycombinator.com/item?id=11611436](https://news.ycombinator.com/item?id=11611436)

And an eli5
[https://www.reddit.com/r/rust/comments/6brtsu/eli5_nonlexica...](https://www.reddit.com/r/rust/comments/6brtsu/eli5_nonlexical_lifetimes/)

~~~
nerdponx
Is there an ELI5 for lifetimes in general? There are so many features of Rust
that I have no context for and no intuition about. I also do not have a CS
degree.

~~~
staticassertion
No CS degree here either.

Variables exist in memory, right? But we only need them for some amount of
time - then we want our memory back. There are many strategies for this - Java
uses a garbage collector to search through memory and reclaim parts no longer
in use, C programmers manually call 'free'. Java suffers from its strategy
because of the performance impact of that scan/ freeing strategy. C suffers
from the complexity of manually managing memory leading to many security
vulnerabilities.

Rust takes this concept of how long memory should live - when it should be
freed - and encodes it into its type system. Similar to how you can say 'this
thing is an int and it can be added to other ints' rust says 'this thing lives
this long and it can reference other things that live this long, or shorter'.

Then the compiler can tell you "hey, you're trying to access something that
might have been freed" the same way it would say "you're trying to add an
integer to a string".

This means you don't have the performance impact of the GC but you also have a
compiler watching your back, telling you when you may be doing something
unsafe.

The algorithm used to determine how long variables live and when they can /
can't be accessed is the subject of the RFC.

~~~
Twirrim
Possibly a stupid question, but is this a bit like reference counting, like
you see in python, only done at compile time?

~~~
rtpg
In python you can't really statically define a reference count

    
    
       a = {}
       b = {}
       l = []
       for _ in range(100):
           if input('thingy'):
              l.append(a)
           else:
              l.append(b)
    

In that snippet it's impossible to know how many references there are to a or
b.

In Rust, lifetime management is set up so that your code can statically
determine when something no longer can be referred to (hence "free GC").

For example:

    
    
       def f():
          a = 3 
          g(a)
          # a is no longer used from this point
          return 5
    

you can clearly define when things are no longer usable, hence free-able.
Python in this context will also free at the end of the function, but it's
because it will be decrementing the reference counter and it gets to 0. But
Rust won't need to have the reference counter since it knows it can free the
object at any point after g.

(This isn't a great example for a lot of reasons, Rust allows you to go much
further than this)

This ends up being somewhat similar to writing code in more statically typed
languages/languages with dependent types. You might have to rework your code
so that the machine can properly identify when it can free memory.

~~~
nerdponx
How then does Rust avoid the if('thingy') issue? Or, for that matter, a
language like C, where presumably a and b would be allocated on the stack. In
C would they both hang around until the function returns? Or would I have to
copy them into the array anyway, which would be heap-allocated?

~~~
steveklabnik
They're stack allocated in Rust too, so they're both going to stick around.
Arrays in Rust are stack allocated, but this code would probably use a vector,
which is heap allocated, and so yeah, you'd end up copying them from the stack
to the heap.

------
solidsnack9000
This is more or less the feasibility test for lifetimes/regions as a general
purpose programming feature. It will probably be many years from now that
another language tries to do anything like this with the same pervasiveness
and breadth.

~~~
tedunangst
Why would that be? If rust proves the concept works, I'd expect more languages
to try it.

~~~
pcwalton
It's very hard to retrofit the rules that the borrow check enforces onto an
existing language. That's because the borrow check enforces "aliasing xor
mutability", and basically all languages in wide use don't enforce that.
Adding that to an existing language generally requires breaking backwards
compatibility in fundamental ways.

For example, all non-immutable objects in garbage-collected languages (or
similar features such as shared_ptr in C++) are incompatible with these
restrictions. As another example, mutable global variables are generally
incompatible with the semantics that the borrow check enforces.

That said, I could see analogous dynamic systems (rather than static ones as
in the case of Rust) becoming more widespread. In fact, Transferables [1] in
JavaScript are an example of one such system.

[1]: [https://developer.mozilla.org/en-
US/docs/Web/API/Transferabl...](https://developer.mozilla.org/en-
US/docs/Web/API/Transferable)

~~~
sanderjd
I read the parent comment's "I'd expect more languages to try it" as meaning
more _new_ languages.

Really interesting point about Transferables, I hadn't seen that.

------
Animats
The big question with Rust lifetimes was "can you tighten the screws down that
tight and still do anything"? Rust managed to get it to work reasonably well.
This lightens it up a bit.

It's not clear to me how far this goes. One annoying Rust idiom is having to
allocate something, then pass it into a function so the function can return
it. There's no way to give a return value the lifetime of the caller, not the
callee. Will this change allow that? It doesn't look like it.

This is more in the other direction. "In the new proposal, the lifetime of a
reference lasts only for those portions of the function in which the reference
may later be used (where the reference is live, in compiler speak)." In modern
compilers, when a local variable has been used for the last time in its scope,
it's dead, and its storage can be reused. This is mostly done to free up
registers. It seems to be exposing liveness analysis at the source level.

The use cases aren't that convincing. This may be more of a "because we can"
feature.

~~~
eddyb
> There's no way to give a return value the lifetime of the caller, not the
> callee.

That is, in the general case, not physically possible, and this sort of thing
only works in GC languages _because_ they return GC pointers to dynamically
allocated data. It certainly doesn't exist in C or C++ (unless I completely
misunderstood what semantics you wanted).

AFAIK there is no implementation of anything (not even JITs) which can
allocate on the caller's stack. The closest anything gets is Forth where the
call stack and data stack are separate and returning just leaves data on the
stack for caller to read.

Without a split stack, returning something larger than a register (or two) is
done by passing a pointer to space on the caller's stack, to the callee.

There have been _very specific_ schemes proposed for `-> [T]` in Rust, e.g.
the slice is left of the _callee_ 's stack, and a pointer to it returned, so
the caller can allocate that much stack space and memmove'd it up.

But that wouldn't help with "allocating" on the caller's caller's stack
because _moving_ the data would invalidate references to it (and if you can
have a reference to something, you can use it in ways the compiler can't trace
it, _unlike_ a GC).

~~~
pjmlp
You can surely allocate on callers stack on C++, that is how return value
optimization works, and was made explicit on ANSI C++17 standard.

~~~
Animats
Right. Just think of the return value as an additional mutable formal
parameter, and function return values as syntactic sugar for that additional
parameter. The caller allocates space for the actual parameter.

~~~
Animats
To be more explicit

    
    
       y = f(g(h(x)));
    

is really

    
    
       var hreturn;
       h(x, hreturn);
       var greturn;
       g(hreturn, greturn);
       // hreturn is no longer live
       var y;
       h(greturn, y);
       // greturn is no longer live
    

Looked at this way, a function can return any fixed-size type without a copy.
Some languages, mostly those in the Pascal/Modula family, did this explicitly.
Instead of having a return statement, within a function, the name of the
function was the return value, and code could assign to it, or pass it by
reference to another function.

------
webkike
Liveness analysis is honestly pretty simple so I'm glad it's being introduced
as a rule for determining lifetimes. I would be afraid of introducing rules
that make for complicated specs, and this is almost one of these areas.
However, it's fairly easy to describe liveness analysis with set theory so I
think it works.

~~~
sanxiyn
Well, analysis is simple, but it does make inference algorithm considerably
more complex. (Analysis generates constraints, inference solves them.)

------
zebraflask
This sounds suspiciously similar to what JavaScript does.

~~~
ZenoArrow
In which way(s)?

~~~
zebraflask
Scope? ES6+? It's pretty obvious both languages originated from Mozilla.

~~~
richardwhiuk
Basically every language since Ada has scope - this is in no way similar to
Javascript.

~~~
zebraflask
Really? Is that why Mozilla is trying to launch a browser written entirely in
Rust? Or was.

~~~
Ar-Curunir
Just because Mozilla worked on both languages doesn't mean they're related in
_any_ way. The goals, execution contexts, and safety guarantees of the two
languages are completely different.

~~~
zebraflask
But unnecessary syntactic sugar seems to be a common trait.

I give up on this thread. :) I can code in both languages, by the way, and I
could swear that Rust is just a systems-level version of JavaScript, or JS
before the hipsters got their hands on it and made it insufferable.

~~~
zebraflask
@pcwalton

I'll take that as canonical, then. I'm probably showing my naivety, but I
don't grasp the reaction to the comparison. Both are basically C-family
languages, there is a distinct Mozilla connection, and recent Rust development
does seem to be web-focused. Rust has a lot of features that one would like JS
to have, or get right, if it does have something comparable.

I think I'm missing some details. Is there a back story for why Rust
developers wouldn't care for that comparison?

~~~
Rusky
Those are all surface-level comparisons, completely outside the actual
languages themselves.

And in particular, this thread is about static analysis of pointers to improve
ergonomics while retaining memory safety. Javascript's approach to memory
safety is just to use a garbage collector, which is essentially the opposite
of the approach taken here.

