[Pitch] Move Function + "Use After Move" Diagnostic

I agree that move(_:), in isolation, is a very ambiguous name. move(_:) isn’t really a function so much as a special syntax for a SIL primitive. Also, what happens if a method on the same type is named move(_:)?

If I were to try to explain move quickly, I’d say “it’s like Python’s del, except it returns the value of the variable”. So… how about a del keyword?

let x = foo()
del x
let y = bar()
baz(del y)

I think a keyword sidesteps the need to require let _ = syntax, because it doesn’t look like a function.

Alternative names for del: unbind, rename, forget.

No, because Python’s del works like what I described earlier, where the binding is killed rather than being marked as unusable. Furthermore, because of Python’s memory model, del actually does delete things.

I did say it was an analogy, not a definition. :slight_smile:

It’s currently named after C++’s move, which leaves the original binding in an undefined state (because of course it does). You are safely able to either destroy or reassign that original.

I think we should abandon the idea of one-word names entirely, at least if it’s going to be a top-level function. “move” is fine, especially given it is the term of art, but not in isolation. It must be clear at the point of use that it operates on the binding, not directly on its value.

1 Like

One will not be able to define such a function for copyable types in Swift without a special compiler diagnostic. It isn't compiler magic, it is a compiler diagnostic.

1 Like

last move in the following example also releases memory:

let x = V()
useX(x)
let y = move(from: x)
useY(y)
_ = move(from: y)
// x & y can't be used here, memory is released

equivalent desugared form:

do {
    let y: V
    do {
        let x = V()
        useX(x)
        y = x
    }
    useY(y)
}
// x & y can't be used here, memory is released

@Michael_Gottesman could you please expound on why (or whether) it's viewed as important to model this as a stdlib function in Swift rather than, say, a keyword?

1 Like

I think it’s most natural to model it as a function, but am open to introducing a keyword if that’s the right path. That being said I do think that there is precedent in the language for this type of pseudo-function with extra semantics: withoutActuallyEscaping. My reason for pushing back on introducing another keyword is that Swift already has a lot of keywords and we shouldn't expand the number of keywords unless we absolutely need to and I don't see that here.

4 Likes

This sounds like something that should be discussed more widely. For example, if there is agreement on this point, that would impact the proposal for existential any.

This is just talking about using a keyword for this specific feature. I am not saying anything about other keywords/uses and whether or not it merits it.

I didn’t mean to construe your post as rendering an opinion on that proposal; I’m saying that it highlighted a potentially unresolved language design question, since the authors of that pitch came to the opposite conclusion about the utility of a new keyword.

For people who are uncomfortable with the name "move", would consume work better? Rewriting the first example from the proposal:

// Consumes x: y's lifetime begins and x's lifetime ends.
let y = consume(x) // [1]
// Consumes y: since _ is no-op, we perform an actual release here.
let _ = consume(y) // [2]
useX(x) // error, x was consumed at [1]
useY(y) // error, y was consumed at [2]

I do feel "x was consumed" is an error message that requires less explanations than "x's lifetime was ended" or "x was moved".

1 Like

Sorry for the delay in response, I was thinking about this and prototyping some things. So I was able to just add support for inout parameters on main: https://github.com/apple/swift/commit/e6faa3048855741db1744ed1ced26d83c3a2cf90. We were having some weirdness on the bots so I don't have a toolchain for you yet, but I hope to have one today soon in this PR.

But yea, so with that PR on main we now support:

    mutating func appendNoError() {
         let b = _move(self).buffer
         let maybeNewB = maybeGetNewB(b)
         self = .init(buffer: maybeNewB)
     }

     mutating func appendError() { // expected-error {{'self' used after being moved}}
         let b = _move(self).buffer // expected-note {{move here}}
         let _ = b
     } // expected-note {{use here}}

sort of diagnostics. The reason why the error is on the last '}' is that I am treating inout verification just like var verification except I give the inout an extra implicit use on all function exiting instructions. That ensures that a valid value must be there before function exit.

I have actually found it to be very useful in catching me forgetting to re-initialize self along try/catch blocks when I was writing test cases, so I imagine it will be very helpful.

With that in hand, I am pretty sure I can handle the defer case as well and handle your test case. But I am hacking on that now. Once I am done with that I am going to update the proposal with these things.

12 Likes

Thank you for explaining—I agree that withoutActuallyEscaping is another good comparison.

The difference, in my mind, between move as-proposed and withoutActuallyEscaping (as well as withExtendedLifetime, and type(of:) two other 'pseudo-functions' that have been mentioned) is that ultimately, they all behave basically like 'true' Swift functions: they accept a value, and produce an output in accordance with all the usual rules of Swift semantics, albeit with some additional type system support that can't currently be spelled in a function signature (e.g., to make sure that the argument to withoutActuallyEscaping is of function type).

For instance, the fact that escaping function types have a subtype relationship with non-escaping types means that all of the following compile, even though they are a bit silly:

func f(g: @escaping () -> Void) {
    withoutActuallyEscaping(g) { escapingClosure in
        print(escapingClosure)
    }
}

func makeF() -> () -> Void { {} }

f(g: {})

withoutActuallyEscaping(makeF()) { escapingClosure in
    print(escapingClosure)
}

withoutActuallyEscaping({}) { escapingClosure in
    print(escapingClosure)
}

Furthermore, potential future language enhancements could make withoutActuallyEscaping much more expressible in the type system. E.g., with variadic generics the signature of withoutActuallyEscaping could look something like:

public func withoutActuallyEscaping<Args..., InResultType, OutResultType>(
  _ closure: (Args...) -> InResultType,
  do body: (_ escapingClosure: @escaping (Args...) -> InResultType) throws -> OutResultType
) rethrows -> OutResultType

However, move differs in a couple of important ways. For one, it does not operate on Swift values. It operates on bindings, and even more specifically on bindings to storage. These are not concepts that the type system exposes to users in any form today, and I am not aware of any plans to expose such concepts to the type system. The type semantics of move are trivial (it's the identity function), but move has implications at both the syntactic level ('is the argument written as a binding?') and a deeper semantic level ('does the binding refer to actual storage?').

Additionally, the move 'function' has effect on the semantics of the code exterior to the call of move itself. AFAICT, this does not apply to withExtendedLifetime or withoutActuallyEscaping whose effects (and all with-style functions) are explicitly constrained to the body of their closure argument.

IMO these are strong reasons to make move a piece of syntax—its integration with the language is much deeper than is able to be expressed by a stdlib function or the Swift type system.

Suppose we were proposing the guard functionality before it was around, and suggested an stdlib function with the following signature (and lifting the restriction variadic auto closures... :slightly_smiling_face:):

func guard(conditions: @autoclosure (() throws -> Bool)..., else: () throws -> Void) rethrows

But that doesn't give us all the power we want from guard, so we additionally propose some 'extra semantics': each of the conditions is permitted to be a pattern, bindings introduced by each of conditions are introduced in the outer scope, and if the else block is executed then guard will return control to the caller two levels up.

Sure, we could have approximated the language-construct version of guard with a function, but it would have seemed very weird to me to pretend that it's 'just' a library function when its interaction with the language and the calling function is really much more fundamental. IMO it's the same for move (though I admit move as a function is a bit less ridiculous than my analogy :slightly_smiling_face:).

EDIT: to make my point a bit more directly (sorry for the rambling post), I think that expressing move as a function actively interferes with the understanding of the feature. Under Swift's function call semantics, as I understand it, both the T passed in to move and the T returned from move should be (semantic) copies, but move is very specifically not copying its argument/return value.

13 Likes

It's not look intuitive. I think it's better to use drop(x) to end lifetime.

1 Like

I like @tera’s point that move takes something akin to an “in” parameter, which we don’t have in Swift today. Perhaps that’s an avenue worth exploring particularly as we think about other pieces of the ownership story.

14 Likes

A few things:

  1. The "in" convention already exists in the language as the default convention for inits and non-self setter args.
  2. Additionally, we already have an attribute override for this using the __owned attribute. That being said, that needs to go through evolution for us to be able to use it. I am hoping to find the time to write a proposal for that soon, but no promises! (It would have a different name though, consuming instead of __owned). NOTE: It is distinct from move itself since consume doesn't imply the special move data flow rule.
  3. move is using __owned, so it does have the "in" convention.
4 Likes

Can someone help me understand why you would ever want to write let y = move(x) (i.e. assign to anything other than _)? The author uses this construct in the pitch, and I'm not sure whether that is a mistake or intentional.

1 Like

Consider the following Swift:

struct SortedArray {
    var values: [String]
    
    init(values: [String]) {
        // Ensure that, if `values` is uniquely referenced, it remains so,
        // by moving it into `self`
        self.values = move(values)
        // Ensure the values are actually sorted
        self.values.sort()
    }
}
7 Likes

can we have let b := a suger for let b = move(a)