[Pre-RFC] `scoped` keyword for guaranteed scoped lifetimes


#1

EDIT3:

This proposal is now superseded. It turns out that the other proposal combined with explicit drop calls can guarantee scoped lifetime already.

But there may be use cases for more fine grained movement control, so a new proposal for that has supersaded this one, though I am not sure if people will be interested in that now that guaranteed scoped lifetimes can be achieved. Maybe after 1.0?

EDIT2:

Currently I am working on the “formal” RFC for this, which will be based on the “formal” and revised version of the other proposal. Some aspects of scoped's and scoped mut's semantics are changed or refined in the “formal” revision. So this proposal here would only be serving reference purposes from now on.

EDIT:

This proposal came before and inspired that one. And now this is edited to be kept in sync with that one.

Summary

Introduce a new keyword, scoped for guaranteeing that a movable object has scoped lifetime.

Motivation:

The motivation is discussed in detail in RFC PR 210’s comments and in this discuss post. As this is not (yet) a “formal” RFC, I would not repeat the discussion here. But please allow me to emphasis the conclusion:

  1. There are cases where we want to ensure movable objects have scoped lifetimes, or rather, we want to explicitly control their lifetimes;
  2. The only way to ensure that a movable object has scoped lifetime is ensuring that it never moves out of the scope before the end of the scope.

Please note:

  1. “the end of the scope” can be the lexical end, or the end(s) created by return/break/continue etc;
  2. though this proposal is inspired by RFC PR 210 and intends to replace the NoisyDrop/QuietDrop part of that proposal, it is orthogonal to static drop semantics/balanced move semantics.

Detailed Design:

Ensuring that an object is not moved out of its scope is like ensuring an object is immutable:

  1. we can do it with discipline, or we can do it with language support, like in Rust today;
  2. a partially moved value cannot be used as a whole any more, so we need inherited movability, which is like inherited mutability.

EDIT: the second point is now addressed by the other proposal.

So I propose that we do the following:

A new keyword scoped will be introduced and used as a variable modifier like mut, as in:

let scoped foo = Bar(...); // a `Bar` object stored in the immutable and scoped variable `foo`.
let scoped mut baz = Qux(...); // a `Qux` object stored in the mutable and scoped variable `baz`.

Scoped variables have the following properties:

  1. anything that has move semantics and can be assigned to a non-scoped variable, can be assigned to a scoped variable;
  2. once stored in a scoped variable, an object can be moved (by assignment) into other scoped variables in the same scope, but not into nested scopes;
  3. all other kinds of explicit moves of the stored object are forbidden, e.g. moving into a non-scoped variable, returning from the function, or consuming by other functions like an explicit drop or a self-consuming method;
  4. all movable parts of the stored object are pinned, i.e. partial moves are forbidden;

EDIT: property 4 is now decoupled from scoped, but added to mutability control semantics, as stated in the other proposal.

Property 1 is to forbid storing objects with copy semantics in scoped variables. Those objects already have guaranteed scoped lifetime.

Allowing the movement pattern in property 2 means the following idiom is still valid for scoped variables:

let scoped mut foo = Bar(...);
... // modifies foo
let scoped foo = foo;

Together, the four properties guarantee that an object stored in a scoped variable to be alive (as a whole) exactly till the end of its enclosing scope (where it gets implicitly dropped/moved into oblivion).

Drawbacks:

A new keyword and increased complexity.

Alternatives:

  1. Maintain the status quo.

  2. Make scoped an attribute, i.e. #[scoped], thus saving us a keyword.

This was the original plan (though #[scoped] was #[lifetime(scope)] there), but as I realized that scoped has much in common with mut, I decided that a keyword may be better. Also, can an attributed variable possess all the properties above?

EDIT2: and with the other proposal extending mut to control inherited movability, it makes more sense to use a scoped keyword now.

Unsolved Questions:

Consider the following:

fn fancy_function(...) {
    let foo = Bar(...);
    ...
    if ... {
        ...
        {
            let scoped foo = foo; // Intention: ensure foo get dropped at the end of this nested scope in a branch.
            ...
            // foo dropped here (implicitly, but the programmer chooses to do so.)
        }
        ...
    } else {
        ...
        // Point A: foo implicitly dropped here if not explicitly moved (static drop semantics).
    }
    ...
    // Point B: foo implicitly dropped here if not explicitly moved (dynamic drop semantics).
}

The possible implicit drops at Point A/B, and any other moves in the else branch may be unexpected by the programmer, as scoped signifies his/her intention to explicitly control the lifetime of foo, even though it only appears in one branch. From this point of view, we at least need a lint/warning.

But technically, there is nothing wrong, as the compiler does fulfill the scoped lifetime guarantee of the "inner foo", and the programmer doesn’t require any lifetime guarantee on the "outer foo". And a situation like this one is so specific that I cannot come up with a succinct name for the lint, implicit_drop_caused_by_moving_into_scoped_variable_in_another_branch? Simply not convincing.

Or we can use more rules to limit the kind of things that can be assigned to a scoped variable so the above snippet becomes illegal, but I suspect the rules would be quite complex and limiting.

So my current take is that just like mut, the programmer should be more careful generally when he/she sees scoped. We cannot statically prevent all programming errors, and that’s fine.

Are there better solutions?