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

I'm just going to throw in that I think moved(from:) is probably a better name than just move(). It keeps the "move" vocabulary while making the use site, let y = moved(from: x) more fluent.

7 Likes

thank you, good point. somehow I only remember using that in relation to try / catch..

1 Like

I like that. as yet another alternative we can use rebind / rebound(from:) for move and unbind for drop.

I think the name move is the best choice, since it's the term of art for this operation. However, I do believe that move(&x) or move x would be more clear about its impact on a variable x than move(x).

6 Likes

I can't help but remember the My favorite rust function article. The article author marvels at the simplicity of the definition of the Rust drop function (see the "Alternatives considered" chapter of the pitch for a mention of drop and its relation to move):

pub fn drop<T>(_x: T) { }

Rust's drop does not need any compiler magic. It works the way it works as a plain application of the general Rust ownership rules. The drop function could be defined in userland!

I do not know if @Michael_Gottesman wishes that Swift eventually becomes able to define the pitched move function without any compiler magic as well. This would clearly be an argument for declaring move as a regular function right now.

5 Likes

swift version could be almost as magical:

func drop<T>(_ v: in T) {} // new syntax for "in" parameters
func move<T>(_ v: in T) -> T { v }

This is not a new special case.

let x: Int
// x is unbound
let y = x // error
x = 32 // OK
let z = x // OK

The only difference with the state after move is that you canā€™t assign a value to it, but thatā€™s consistent if you interpret the current rules as ā€œx can only be assigned to onceā€ rather than ā€œx can only be assigned to if unboundā€.

8 Likes

another drop alternative:

let x = 1
unlet x
var x = "hello"
unlet x

Fully agree that this is the simplest mental model that we should be shooting for. And Iā€™d go further and say that generally if we can make the feature describable without reference to binding (which is really leaked implementation detail from the compiler) that would be a win for simplicity.

I wonder if we could key in on the memory release aspect as more concrete.

let x=somethingBigAndMemoryHungry
var y=release(x)
let pointer: UnsafePointer<SomeStruct> = ...
_ = move(pointer)

Memory leak?

Yeah, this is what I have been trying to get at as well. I would really appreciate an elaboration on how move is expected to fit into the future of Swift ownership.

6 Likes

IIUC, this wouldn't be more of a memory leak than having this:

do { // or any other scope
    let pointer: UnsafePointer<SomeStruct> = ...
    // end of scope
}

Somebody correct me if I'm wrong.

2 Likes

In your example - yes, definitely.
But someone might think that the memory will be freed with _ = move(pointer).

Is the "new" version below a syntactic sugar for "existing" version? Just written differently without curly braces (which is not an insignificant thing).

// new
let x = 1
let y = move(from: x)
// can't use x here

// existing
let y: Int
do {
    let x = 1
    y = x
}
// can't use x here

// new
let x = 1
print("use x \(x)")
drop x
let x = "hello"
print("use new x \(x)")
drop x
// can't use x here

// existing
do {
    let x = 1
    print("use x \(x)")
    do {
        let x = "hello"
        print("use new x \(x)")
    }
}
// can't use x here

Adding a couple examples from the pitch, proposed form and "equivalent" desugared form:

// new
let y = move(x)
let _ = move(y)
useX(x) // error
useY(y) // error

// existing
do {
    let y: ...
    do {
        let x = ...
        let y = x
    }
    useX(x) // error
}
useY(y) // error

// new
func test() {
    var x: [Int] = getArray()
    x.append(5)
    var y = x
    // ... long algorithm using y ...
    let _ = move(y)
    x.append(7)
}

// existing
func test() {
    var x: [Int] = getArray()
    x.append(5)
    do {
        var y = x
        // ... long algorithm using y ...
    }
    x.append(7)
}
1 Like

I think you're right. Some of us use do { } as a way to limit the scope of local variables, or as a way to force the compiler to release a variable / call a deinitializer. The move function can be seen as a way to avoid those do { } blocks.

But you have seen that the motivation chapter of the pitch does not talk about do { } at all - and this is a hint that move is about a more general language feature. If I understand well, it is all about unlocking efficient usage of memory. For example, when one deals with copy-on-write values, it is important to avoid unwanted copies in order to keep under control both the complexity of the algorithm and its memory usage. This is the topic of the "introduction" chapter of the pitch.

5 Likes

We need some killer example that shows that the new proposed feature can do something that do {} can't do at all or is too cumbersome. For me personally reducing curly braces nesting is not a small thing, and it alone would be enough motivation; just trying to understand if there is more to it than just that.

That Thank you for linking the manifesto. It should be noted that the ownership manifesto is (years) out-of-date in at least some regards, which is why I think itā€™s important for this proposal to be a self-contained justification for the feature it introduces.

If the elaboration required to explain how move fits into the ownership story is too large, or too irrelevant to the feature itself, then I think we need an updated version of the manifesto and/or a guiding document like the concurrency roadmap to help understand how ownership features will fit together.

If the rest of the ownership model isnā€™t at a point where it will be ready for review soon, I donā€™t really understand how we can properly evaluate the individual pieces.

10 Likes

If a function parameter attribute ends up being the chosen path, I'd prefer something more verbose (and more searchable) than in. Like:

func drop<T>(_ v: consuming inout T) {}
func move<T>(_ v: consuming inout T) -> T { v }

I still maintain that Swift.move(_:) is not an appropriate name for the function: it is far too ambiguous.

Contrast with Swift.MemoryLayout.size(ofValue:): no one could confuse it, for instance, with the cardinality of a collection.

1 Like

For declarations prefixed with ā€œunsafeā€, sharp edges are part of the territory. In the case of UnsafePointer, the documentation makes it very clear that you must employ manual resource management, and messing up may result in undefined behavior.

If it is documented, it is not an implementation detail. Furthermore, simplicity is not a justification for being inaccurate. This function ends the lifetime of a binding/reference. That does not mean anything is unloaded from memory, as other references may keep it alive.

In the ā€œArray/Uniqueness Exampleā€ of the original pitch, y may or may not be a copy of x. If it isnā€™t, move(y) does not cause a release, as x is still alive.

If you really want manual resource management for some reason, just use Swift.Unmanaged.