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

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)

Why? Have any other languages used this syntax for move? Whereas several languages have used it for assignment.

let b <- a

would be more natural here. but then how best to drop?

_ <- a
nil <- a
unlet a
drop(a)
unbind(a)
remove(a)
_ = move(from: a)
...

Because I want move is more simply to use than copy, but making move default on COW type will be source breaking

Pitching and taking consuming (née __owned) through evolution before the move function would probably clear up a lot of the confusion here.

6 Likes

When Swift can declare move-only values (on the caller side, or as part of the type), that's exactly how the move or drop function will work, without any magic, just like the Rust examples. The compiler Builtin inside move's implementation will be superfluous in that case. The nice thing about the move that we're proposing is that it has the same semantics for any argument value. That does require magic to distinguish normal pass-by-value semantics from move semantics.

There are alternatives to using compiler magic to handle copyable argument values. I'm not a syntax afficionado, and I don't see any precedent in the language for something like this, but we should at least consider them:

  1. A move keyword that works as an operator

foo(move arg)

  1. A move parameter modifier

A free-standing move function could be written as

func move<T>(_ t: move T) -> T {
  return t
}

Or even expressed as a prefix operator.

Any user-defined function could force argument moves.

This runs counter to the current Swift practice in which parameter modifiers only affect semantics on the callee-side. Normally, without any recognizable syntax on the caller-side, argument semantics are unaffected. For example __owned does not currently force a move (nor should it).

7 Likes

consume is good, but it will be overloaded with other "consuming" uses, which consume their argument value, but do not prevent implicit copies and subsequent uses of the variable. move consumes its value and also ends the variable lifetime.

3 Likes

Just because something can be spelled a certain way doesn’t mean it should be spelled that way. For example, functions can implement the exact same semantics of computed properties. But Swift chooses to give computed properties the same syntax as stored properties because it helps the programmer’s understanding.

I would argue that modeling move as a function harms the programmer’s understanding. The classical definition of a function is a map from domain to range. In this light, move(_:) is the identity function. It’s only when you expand the definition of function to include its effects on the calling environment that you can even start to explain the semantics of move(_:).

As you illustrate, there’s a good chance that a keyword will be required at the call site to indicate that the client is unbinding the value. Why add the extra layer of function call syntax atop that? Let the keyword do the heavy lifting.

7 Likes

Would it be possible to somehow reference the target binding, rather than using the assignment operator? Assignment isn’t what’s happening, after all. I think that’s the crux of the issue here.

struct SortedArray {
    var values: [String]
    
    init(values: [String]) {
        // Ensure that, if `values` is uniquely referenced, it remains so,
        // by moving it into `self`
        move(\values, to: \self.values) // Syntax of local key path expressions subject to change
        // Ensure the values are actually sorted
        self.values.sort()
    }
}

That would still require a tweak to the grammar to allow local key path expression literals (which need not resolve to a public KeyPath type for the moment), but I think it’d fit what is actually happening much better: you are operating on bindings, not values.

For that to work, you’d also need some way to make new declarations with let or var, ideally while preserving type inference, so obviously the idea needs some work.

1 Like