Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

'fn lifetime ascription #1847

Closed
wants to merge 9 commits into from
Closed

'fn lifetime ascription #1847

wants to merge 9 commits into from

Conversation

llogiq
Copy link
Contributor

@llogiq llogiq commented Jan 7, 2017

This is a much reduced version of lifetime ascription that gives us none of the great teachability of full lifetime ascription, but rather extends the power of the lifetime system by allowing to ascribe a 'fn lifetime.

(rendered)

@llogiq llogiq mentioned this pull request Jan 7, 2017
@Ericson2314
Copy link
Contributor

In llogiq#1 added and unresolved question for break 'fn.

@comex
Copy link

comex commented Jan 7, 2017

  • In your example, you have 'fn in a struct field. How would this work if values of that type were passed to or returned from functions? It seems to me that 'fn would have to refer to a different lifetime per function, so it would have to be passed to the struct as a generic parameter.

  • What syntax would be used to give a value 'fn lifetime? Would it ever be inferred? As written, your example is clearly a type error as Some(head) is an Option<Node>, not Option<&'fn Node>.

  • What kind of code should the compiler generate if types with destructors are allocaed?

  • Would this provide any way to allocate dynamically sized arrays on the stack?

@whitequark
Copy link
Member

In your example, you have 'fn in a struct field. How would this work if values of that type were passed to or returned from functions? It seems to me that 'fn would have to refer to a different lifetime per function, so it would have to be passed to the struct as a generic parameter.

'fn is an implicitly bound lifetime that is equivalent, borrow checker wise, to the following code in today's Rust:

fn foo<'fn>() {
  ...
}

No changes from the behavior of the code above with respect to borrow checker are needed.

What kind of code should the compiler generate if types with destructors are allocaed?

Destroyed before the function returns, just as if the respective let bindings were hoisted to the very first line of the function.

Would this provide any way to allocate dynamically sized arrays on the stack?

Not without #1808.

Add unresolved question of `break 'fn ..`
@Ericson2314
Copy link
Contributor

Also a nit on terminology. With type ascription, we constrain the type of an expression with an annotation, but here we are binding new lifetime identifier. I'd think that actual lifetime ascription would be using an explicit (already bound) lifetime with the & operator. We should pick a different term like "explicit named lexical lifetimes"

@Ericson2314
Copy link
Contributor

Ericson2314 commented Jan 7, 2017

@comex the example makes sense if it is supposed to be all within a function body. @llogiq maybe make the function explicit syntax types defined within a function are somewhat exotic?

@comex
Copy link

comex commented Jan 7, 2017

@Ericson2314 Having function-local bindings (in this case lifetimes) apply to nested items would be a significant change, albeit one that I'd like to see.

@whitequark Yes, but how is that implemented? Shall there be an implicit linked list of allocaed values to call destructors on? ...Actually, scratch that; this doesn't even make sense for values with destructors because there's no way to translate the strictly-outlives rule. I don't see how you'd prevent a situation where two structs with Drop impls have 'fn references pointing to each other.

@Ericson2314
Copy link
Contributor

Ericson2314 commented Jan 7, 2017

@comex excellent points. I might argue however that the latter is a prexisting problem we just duck by not allowing the user to ask for specific lifetimes.

Also fix type error with missing & operator
This helps make diffs more readable because there is no longer
manual rewrapping every time.

I propose this as a general policy in rust-lang#1811, but absent one right
now I think its fine to do this already.
@Ericson2314
Copy link
Contributor

Ericson2314 commented Jan 7, 2017

Hmm. So currently @whitequark's desugaring doesn't work very well: https://is.gd/7vWrMK. But according my formalization in https://internals.rust-lang.org/t/a-stateful-mir-for-rust/3596 or (if you squint) @nikomatsakis's outlives-at relation (where I got the idea), it's OK to borrow for an already-entered lifetime. This would make that example valid.

But this is some thorny stuff to figure out for our type system (actual axioms for lifetimes, not just a magic borrow checking algorithm we trust!), so perhaps it is easier to start with full named lexical lifetimes.

@ahicks92
Copy link

ahicks92 commented Jan 7, 2017

Why not just move to full lifetime ascription?

It seems to me that we could have an RFC for it that says we'll implement some parts before others as opposed to this and presumably an RFC for that in future.

I'm kind of assuming that something about this proposal is easy to implement.

What about compatibility concerns with 'fn already being used by user code? Is 'fn already forbidden because fn is a keyword?

@comex
Copy link

comex commented Jan 7, 2017

@Ericson2314 Hrm, can you clarify "OK to borrow for an already-entered lifetime"? Normally of course if you have an &'a Foo (for a lifetime parameter 'a) you can send it to the current function's caller by returning it, storing it in a reference, etc., so it wouldn't make sense to give a stack variable that type. I read @whitequark as assuming that 'a is unique and not referenced by anything else in the signature, in which case it works out, but that doesn't seem to match up with what you're saying.

@llogiq
Copy link
Contributor Author

llogiq commented Jan 7, 2017

@camlorn that is the long-term plan, but the 'fn lifetime will give us more bang/buck short-term and works quite well as an extension even if we move to fully ascribed lifetimes, because function block labels look weird.

@ahicks92
Copy link

ahicks92 commented Jan 7, 2017

@llogiq
I agree that this is forward compatible, but the following doesn't seem odd to me:

fn foo() { 'fn
...
}

It's consistent in that it's the same, at least supposing the syntax is { 'lifetime. I don't see why the top level should be different.

@llogiq
Copy link
Contributor Author

llogiq commented Jan 7, 2017

@camlorn the syntax that labels use (and we don't want to change it) is 'lifetime: { .. }. The text even has an example of this.

@llogiq
Copy link
Contributor Author

llogiq commented Jan 7, 2017

Also, yes, fn is a keyword.

@Ericson2314
Copy link
Contributor

Ericson2314 commented Jan 7, 2017

@comex so

let b = ..;
'a: {
  let x: &'a _ = &b;
}

is for sure OK, but we want to effectively do

'a: {
  let b = ..;
  let x: &'a _ = &b;
}

where the lifetime of b only properly outlives 'a if we exclude everything that happens before the borrow.

@Ericson2314
Copy link
Contributor

Ericson2314 commented Jan 7, 2017

@comex hmm this may still be irredeemable because I think we ought to treat the destruction of a as happening before the function body ends. [That is the approach I adopted in https://internals.rust-lang.org/t/a-stateful-mir-for-rust/3596 as part of making drop glue explicit in safe stateful-MIR.]

```rust
struct Node<T> {
data: T,
next: Option<&'fn Node>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uhm, this can't possibly work, you need to make it a parameter of Node otherwise you end up with C++-level handwavey rules that are (approximately) checked through invasive heuristics.
Either that or the compiler injects the parametricity but that's much harder to do and get right than the existing lifetime system that we already have.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eddyb are you treating this as a item defined in a function body (concrete 'fn lifetime), or as some late-bound placeholder? Certainly the latter in untenable, but the former could possibly work.

@Ericson2314
Copy link
Contributor

@llogiq btw if you didn't see it, llogiq#2 more commits for you to merge/cherry-pick/steal-from/whatever.

@eddyb
Copy link
Member

eddyb commented Jan 8, 2017

(moved from diff comment)

@Ericson2314 If we had nested generics (which we don't), the former requires injecting an extra parameter, which is supplied by uses in the surrounding function, which is my second point.
There is no need to allow it in this form, and I don't think @whitequark was thinking of doing anything about type declarations (I sure wasn't), only borrowing expressions.

That is, &'fn Node { ... } to allocate one in the current stack frame, dynamically.
Oh btw you can't infer this allocation thing from the type, it has to syntactically be in the borrow expression for it to be usable to place it in the right scope accordingly.
Which leads to... drops. How would one even drop such a thing? A linked list of dynamic allocations and their associated drop glue? This might actually require something that goes with placement-in to allocate the raw space on the stack, and to be used with custom-allocator-enabled containers.

@Ericson2314
Copy link
Contributor

Ericson2314 commented Jan 8, 2017

@eddyb Makes sense. Here a and b I assume would be the same, but the point is with 'fn one can dynamically allocate?

fn foo<'a>() {
    let x;
    {
        x = Box::new(1u32);
        let a = Box::new(&x);
    }   
}

fn bar<'a>() {
    {
        let b = Box::new(&'fn Box::new(1u32));
    }
}

@llogiq sorry I should have thought more about the motivations for this coming from the alloca RFC.

@eddyb
Copy link
Member

eddyb commented Jan 8, 2017

@Ericson2314 That looks about right, and if you decide to go with a dynamic per-stack-frame "drop list" (which I believe is what Go has w/ defer? @pcwalton keeps mentioning it), it would presumably work.

@llogiq llogiq mentioned this pull request Feb 21, 2017
@arielb1
Copy link
Contributor

arielb1 commented Feb 24, 2017

With the current semantics of Rust, forcing borrows to some lifetime won't help you - lifetimes don't affect the destruction order of locals.

What you do want is to create locals without a corresponding drop (I don't think we can't make destructors run without having a compiler-generated linked list, including the drop flags, per local, which I don't think is a particularly smart thing to do), so they are "leaked" on the next reinitialization. That's why I think the syntax should be on the local. For example,

fn foo() {
    // here we ascribe the `'fn` lifetime to `next`
    let leak start = Node { data: 0, None };
    let mut head = &start;
    for i in iter {
        let leak cur = Node { data: i, Some(head) };
        head = &cur;
    }
}

@arielb1
Copy link
Contributor

arielb1 commented Feb 24, 2017

@whitequark

Looking at your alloca versions, the only borrow checker change needed would be to have it ignore borrows on "previous" instantiations of a local when looking at borrows for the current instantiation - and the only reason you care about borrows of "previous" instantiations is for destructors, which I'm not sure we want.

Basically the "dropless" version is equivalent to supporting:

let mut x0 = 0;
let mut x1 = 1;
let mut x_ = &mut x0;
let z = &mut *x_;
x_ = &mut x1;
use(z);

Adding 'fn and block lifetimes to Rust should not be that hard - we already have the scopes available, we just need to wire up the syntax.

@whitequark
Copy link
Member

@arielb1 I have some doubts the "dropless" version will be accepted, seeing as it's quite non-orthogonal.

@llogiq
Copy link
Contributor Author

llogiq commented Feb 24, 2017

Sorry, @whitequark, but I ran out of steam before completing the lifetime ascription RFC. There are a few interactions we may want to think about (for example ascription should be able to extend, but not restrict the lifetime).

@eddyb
Copy link
Member

eddyb commented Feb 24, 2017

@arielb1 My intention with &'a rvalue is not to only hint a lifetime, but force allocation in an 'a scope.
If there are any loops between the 'a scope and &'a rvalue, you have to either ban it, force dropless or have a linked list of destructors dynamically alloca'd at the same time as the data itself.

EDIT: This is also why I don't use the syntax &rvalue: &'a T because that shouldn't affect drop scopes.

@eddyb
Copy link
Member

eddyb commented Feb 24, 2017

Another suggestion is roughly Stack::<'a> <- rvalue, which would be a fancier version of &move.

@arielb1
Copy link
Contributor

arielb1 commented Feb 24, 2017

Forbidding types with destructors seems like a smarter choice then leaking, indeed.

@aturon
Copy link
Member

aturon commented Apr 29, 2017

This RFC has gone quiet, without much clear conclusion. @whitequark, what's your current thinking here? How do we make progress on this front?

@whitequark
Copy link
Member

@aturon It seems like this is the result of:

  • the RFC really being four RFCs in a trenchcoat, with a bunch of fine detail in each;
  • a lack of clear understanding of whether this four-RFC plan is even a right direction (no one objected, but I'm not sure how many people paid attention);
  • requiring a significant amount of grunt work and re-work, which exhausted @llogiq, who I believe did most of it.

I'd like to hear from other @rust-lang/lang team members whether the overall set of parts that ought to eventually fit together seems acceptable. If it is... personally I won't have time to get an RFC through in at least 1-2 months, perhaps @llogiq could take another stab. I am still quite interested in the features and going to participate in the design process, of course.

@aturon
Copy link
Member

aturon commented Apr 29, 2017

@whitequark Thanks much!

I propose that we hold a dedicated meeting with various interested stakeholders, and try to hammer out some of these questions at high bandwidth. (Either as a lang team meeting or a separate ad hoc one.)

Anyone who would like to be included in such a meeting, please let me know.

@whitequark
Copy link
Member

@aturon Sounds good!

@aturon
Copy link
Member

aturon commented May 3, 2017

@whitequark @nikomatsakis @eddyb any pointers on the stakeholders to include? I believe this was discussed at the compiler sprint?

@nikomatsakis
Copy link
Contributor

Well, we discussed primarily the "unsized rvalues" RFC (#1909), which is certainly a way to support alloca, though it is somewhat orthogonal from this RFC I believe.

I think that what I personally want out of this RFC is not necessarily the same as what @Ericson2314 wanted. I am primarily interested in a better way to teach Rust and to teach lifetimes in particular. To that end, I think that having explicit syntax can be a great way to expose the mechanics. So I'd like to be able to give names to blocks and so forth (e.g., 'a: {...}) and then refer to those names from borrows or other expressions &'a foo. This would let you illustrate the rules in a very explicit way. I had not originally envisioned any special 'fn lifetime, but that seems like it could be a handy addition.

So, I suppose in terms of "stakeholders", I could see two groups. If we're talking about allocas, then I would include @arielb1, who put a lot of effort into #1909, and naturally @eddyb.

If we're talking about teaching lifetimes, I might include other people (@wycats, perhaps).

@strega-nil
Copy link

I'm interested in better ways of teaching lifetimes.

@Ericson2314
Copy link
Contributor

Ericson2314 commented May 11, 2017

@nikomatsakis I think our interests are closer than you might imagine :).

If nothing else, my desire for move and out pointers leads me to be wary of the sublety of unsized rvalues and friends without them. I think the teaching portions of all of this (and IRC I originally thought this RFC was sidestepping the allocation stuff) are wanted by everyone, so let's factor them out and expedite them. I absolutely agree with @whitequark's division of labor—I'd just rearrange the teaching ones first.

I do like breaking out of all blocks, and break 'fn as a synonym for return. Maybe this is my battle alone :), but semantically I see it simpler than any alloca, and good for teaching too as it unifies previously disparate concepts. In any event, that would become a 5th RFC, whose fate is divorced from the rest.

@joshtriplett
Copy link
Member

In #2137 , @canndrew mentions that this would help for defining the right lifetime for a VaList type. The mechanism for non-movable types might also help for that, in some circumstances.

@aturon
Copy link
Member

aturon commented Sep 6, 2017

I'm going to propose we postpone this RFC; we don't have bandwidth to convene the full stakeholders ahead of the impl period, and should revisit this area in 2018.

@rfcbot fcp postpone

@rfcbot rfcbot added the proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. label Sep 6, 2017
@rfcbot
Copy link
Collaborator

rfcbot commented Sep 6, 2017

Team member @aturon has proposed to postpone this. The next step is review by the rest of the tagged teams:

No concerns currently listed.

Once these reviewers reach consensus, this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@llogiq
Copy link
Contributor Author

llogiq commented Sep 6, 2017

For the record, I'm fine with this, hopefully I have the energy to revisit it next year.

@rfcbot
Copy link
Collaborator

rfcbot commented Sep 19, 2017

🔔 This is now entering its final comment period, as per the review above. 🔔

1 similar comment
@rfcbot
Copy link
Collaborator

rfcbot commented Sep 19, 2017

🔔 This is now entering its final comment period, as per the review above. 🔔

@rfcbot rfcbot added final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. and removed proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. labels Sep 19, 2017
@aturon
Copy link
Member

aturon commented Sep 29, 2017

I'm going to close this RFC as postponed for the time being, as per previous comments; we hope to revisit after various other dust has settled! Thanks @llogiq!

@aturon aturon closed this Sep 29, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.