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

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.

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.