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

The proposed function doesn't move the binding.

  • if you are someone who expects to see __owned made into a general language feature at some point, then any argument sigil (such as &) that is chosen for move will also have to become the argument sigil we use to pass anything into any function that has the same ownership semantics as move.
func moveInitializeCudaArray(at pointer:UnsafeMutableRawPointer, 
    from x:owned [Int]) -> [Int]

let x:[Int] = [1, 2, 3]
let y:[Int] = move(&x)
// headers and backing storage of `z` now live at `pointer`
let z:[Int] = moveInitializeCudaArray(at: pointer, from: &y)
  • if you are someone who expects to see __owned made into a general language feature at some point, a language keyword such as del or unlet will immediately become redundant when that happens. removing them from the language will likely have to fight its way through the typical litigation of swift evolution, and will probably require some type of staging plan to move people off of the keyword.

  • we already have UnsafeMutablePointer.move. we do not have UnsafeMutablePointer.consume.

  • consume(_:) is not a good name for move(_:), because move(_:) returns its parameter to the caller. it would make sense to have a consume(_:)-like variant of move(_:), which returns Void, but it should probably be called deinitialize(_:) to be consistent with the rest of the language.

let x:[Int] = [1, 2, 3]
let _:Void  = deinitialize(x)
  • move(_:) is a common function name, and most people will adapt to this new reality by spelling it Swift.move(x).

  • present- vs past-tense for mutating vs returning methods only makes sense for instance methods. move(_:) is not an instance method.

  • in the following example, foo(s:) is to removeAll what bar(s:) is to move(_:). neither of these compile.

struct S 
{
    var x:[Int] { [1, 2, 3] }
}

func foo(s:inout S) 
{
    s.x.removeAll()
}
func bar(s:S) 
{
    move(s.x)
}

move seems very special and much more than just a small new funtion, so I'm quite happy that in the meantime, A roadmap for improving Swift performance predictability: ARC improvements and ownership control popped up.
Maybe that other thread will shed some light on the topic, but until than, I want to suggest a variation that afaics has not been mentioned yet:
As move is not even a regular function at all, but rather a hint to trigger some compiler feature, why not use the "compiler magic prefix" and call it like self.x = #move(&parameter)?

2 Likes

I'm not an expert in this field, but I understood the proposal, especially after reading through The Roadmap which makes a good case for introducing this.

My only question is why is move (and future copy) being introduced as a function rather than a keyword like return? It obviously does something that regular functions can't do and reminds me a lot of the discussion on any P versus Any<P> where the keyword was chosen.

2 Likes

That is somehow deeply painful to think of as Swift code.

Honestly, I’ve been waffling back and forth on whether to advocate for making this a keyword. I’m currently leaning towards keywords again, especially if it isn’t going to be put in a namespace. The risk of mistaking it for a normal function is too high.

Swift has been commonly criticized for having too many keywords, but I’m not entirely sure it’s a problem. If something doesn’t play by normal rules, having it stick out is a good thing.

Many people mistake that for boilerplate, but when I read Swift code I never see anything extraneous: every part always conveys information to either the compiler or the reader.

3 Likes

FYI, I posted a pitch for consuming/nonconsuming here: [Pitch] Formally defining consuming and nonconsuming argument type modifiers.

2 Likes

I've come around to supporting the functionality of this after reading Joe Groff's excellent roadmap, but am not a fan of the wording. move to me suggests something else, or a number of something elses. It's not really what's going on here. Here the object is being deleted from memory, so it can't be accessed afterwards. Whether something else retains a copy is secondary.

And using _ = move() is confusing. _ = XX. suggests XX is being evaluated, i.e. it's being moved, but then the result thrown away. But that's not really what's happening. And the English language can be used in a way already used elsewhere in Swift to avoid this.

E.g.

// to dispose of x call release
release(x)
// but if a copy is needed call released
y = released(x)

This mirrors the use of e.g. sort and sorted for Array. released is just a nice way of writing out both the copy and release, i.e. is identical to

{
    y = x
    release(x)
}

Doing it this way puts the release first as I think it should be. "release" has other meanings in computing but in code normally means something like this. Other possibilities include free & freed or dispose & disposed. I actually thought of expire and expired first but that's probably too odd.

3 Likes

i think the main reason for introducing move() is to move binding, for e.g.:

let y = move(x)

move() can be used for ending lifetime of binding, for e.g.:

let _ = move(x)

but i doubt "to end lifetime of binding" is the main reason for introducing move() function.

2 Likes

But that is what's happening. For _ = move(x) the value is being moved out of x then thrown away. The release is a side effect.

3 Likes

I keep thinking about the name of this and the fact that is just a function that can't be implemented in the language.

In my mind it would be perfect if the language could implement it int he same way that is implemented in Rust.

And after reading the roadmap it feels like it can get close but not completely....

So the purist in me feels that it shouldn't be a function. But then again, we see to have other functions in the language that are not implementable at user space so maybe a more pragmatic viewpoint is needed.

I think the problem is deeper: It's hardly a normal function at all...
Implementing the function-part is trivial (it returns the value you put in, so it is just identity), but the side effect is extremely strange (when comparing with ordinary Swift):
It's not only able to somehow change a value declared with let, it even "destroys" its argument, so that it can't be used anymore at all.
Given the fact that Swift is quite restrictive when it comes to redeclaring something without creating a new scope, this is a massive change (for what a function can do).

2 Likes

Moved out of and thrown away, yes, not moved. move on its own suggests something quite different. It could be called moveOutOfAndThrowAway but that's not very Swifty. But there are single English words which better describe what's happening, such as those I suggested.

It seems to me that one part of the syntax-level dilemma is that there's two ways of looking at move():

It destroys a binding, yielding the value previously referred to by that binding, which is not a concept expressible in Swift, and therefore feels "keyword-y"

or

It sets the value referred to by a binding to be uninitialized and returns the old value, which is mostly a concept that already makes sense and even mostly already exists (since you can have a variable declared with no initial value, and it'll error if used), and therefore feels a bit more "function-y".

Like you can almost imagine implementing move() like this:

func move<T>(_ x: inout T) -> T {
  defer { 
    x = uninitialized //doesn't actually exist
  }
  return x
}

I'm almost tempted to suggest = uninitialized as a spelling for this in fact, except that

foo(x)
x = uninitialized

is clunkier than

foo(move x)

(and might have ordering issues in more complex uses, since it's not an expression)

7 Likes

Indeed. And if we want to go even further, the next stage of "variable lifetime" could be "undeclared again" to return situation back to the state where you write "var x: T" (potentially with another type) again. In the scoped version this is equivalent to exiting the inner scope.

let x: A
foo(x) // error: uninitialized
x = expression
foo(x) // ok
uninitialize x // pseudo syntax
foo(x) // error: uninitialized
let x: B // error: already defined
unlet x // pseudo syntax
let x: B // ok

Sorry to ask this again: are there cases that can be elegantly expressed by move/drop machinery but can't be expressed by do {} scopes (or are too cumbersome in the latter form)? In other words is move a mere syntax sugar? Would like to see some killer example (with, say, 5 lines with new move machinery and 30 equivalent lines with old do {} scopes method).

let moving:[Int] = [1, 2, 3]

                  (moving)
                   (moving)
                    (moving)
                     (moving)
let moved:[Int] = move(moving)

If you’ve ever written any code that uses locks, you’ve probably encountered situations where cleanup happens on several paths, but at different times or in different orders. That’s a nightmare to do solely with scopes. Rust has shown how similar locking and memory safety actually are, so I suspect there would be similar caes for move().

4 Likes

Apologies if this idea has been discussed and dismissed elsewhere but I agree with the sentiment that move is a very unusual function compared to any other, which makes me think that it should be a keyword and not a function. Why should move be a function and consuming be a keyword? I know the latter kind of has to be but this seems inconsistent to me. I understand the motivation to make this a function to work with a cute usage like “_ = move(x)”, but do we really want our code to contain lines that look like that? A beginner would likely have absolutely no idea what that code would do, even if consumeX(move x) is fairly innocuous.

In addition, as an expert feature it seems too easily accessible IMHO, which makes me think it’d be too easy to get it wrong. Easy things should be easy, expert things should have some kind of warning sign attached, which IMHO a keyword would provide.

One other thing comes to mind, how does this interact with inner scopes: (edit: the detailed design suggests that the following would not compile after all - to be honest I’d understand why but it does mess with my understanding of the feature and / or scoping somewhat)

func foo() {
    let x = MyType()
    do {
        _ = move(x)
    }
    print(x) // possible?
}
1 Like

Given the example of moving inside an if statement from the proposal, that print should be invalid as x is uninitialized.

1 Like

Just a thought: given that a function that takes an argument provided by move is consuming, was there consideration given to calling the function (or keyword..) consume?

_ = consume(x) would be much clearer to a beginner than _ = move(x) IMHO.

Just a quick update. I was able to implement the defer handling that was asked for up thread. If you want to try it out, check out the toolchains attached below.

On another note, I looked into the keyword solution that people were talking about in the thread above. Turns out changing move and (and referencing the roadmap thread) copy to be keywords is likely to cause source breaks. All methods with the name move, copy would have to be modified to use backpacks "`". In contrast the function approach will keep all such methods working (that is no source break) and one would still be able to use move/copy, but would need to refer to the full name of the function: Swift.move, Swift.copy.


Toolchains:

macOS: https://ci.swift.org/job/swift-PR-toolchain-osx/1272//artifact/branch-main/swift-PR-40518-1272-osx.tar.gz
Linux: https://ci.swift.org/job/swift-PR-toolchain-Linux/772//artifact/branch-main/swift-PR-40518-772-ubuntu16.04.tar.gz

The PR: [DRAFT] Just for toolchain by gottesmm · Pull Request #40518 · apple/swift · GitHub

3 Likes