SE-0366: Move Function + "Use After Move" Diagnostic

Agreed, but you seem to be implying that move semantics will be "a seldom used niche feature". I agree it is an "advanced" feature, but as soon as you start having widely used libraries defining move-only types, users of those libraries will have to use these features in practice.

One major point of operators is that they are visually distinct. Furthermore, Swift's approach is a benefit here: unlike C++'s operator overloading, Swift allows defining /new/ operators. Novel operators are better for conceptually new things like this, because they allow a reader to "realize that this is something they don't know".

2 Likes

It might help to clarify the future of move in a world with move-only types/values.

Calling the move intrinsic on a move-only value would have no functional effect, except when used as a drop function. Even as a drop function, the intended behavior is identical to optimized builds. (There will need to be a separate discussion about whether debug builds should eagerly drop move-only values.)

When move is used with move-only values, it will be a form of self-documentation. Either the programmer used it correctly, and it does nothing. Or the compiler issues an error.

I view move mostly as a tool to use move semantics with copyable types with the highest possible precision--very much an advanced feature. But still an important feature because, when you do need it, it's on the critical path.

5 Likes

I appreciate the additional information that various members of the Swift team have provided. It’s good to hear more about how move(_:) fits into the larger vision for move semantics, and how it differs from the C++ and Rust features that inspired its name. I think that context is necessary to properly evaluate this proposal, because without it I definitely got the impression that move(_:) was designed to be the core interface to Swift’s move semantics, à la std::move(). @Andrew_Trick says “move-only types […] will be a much more visible language feature”, and I am also told that move(_:) is explicitly not meant to address certain use cases such as guaranteeing in-place mutation.

Filling out the design

Even with that additional context, I do think the proposal under-specifies some aspects of move(_:)’s behavior. The interaction with captures is significantly underspecified, IMO. I’ve learned some things about @escaping in this thread that are not explained in TSPL. Perhaps if I were an expert on every nuance of closure behavior the proposal contains enough information for me to infer the complete interaction between move(_:) and captures. But like most people who will eventually use this feature, I am not an expert on any aspect of any of Swift’s behaviors, and I still don’t quite understand whether the compiler is expected to accept or reject capturing a variable that is at moved from at some future point in time.

Likewise, I think the proposal could do more to explain how move(_:) in argument position interacts with (and possibly redefines) Swift’s formal model of inout parameters, which have heretofore been defined as a copy to and write-back from an invisible temporary variable. Does that mean values must be moved into inout arguments by the caller to avoid a copy? What happens if the caller does not move the value into the argument, but the callee does try to move the value out of the parameter? Is it possible to define the new semantics without having to introduce something akin to the prvalue/xvalue family tree?

The spelling of the operation

As the proposal admits, move(_:) looks like a function but it’s not a function. The presence of the move(_:) token itself is what imparts its effects. One might be tempted to draw an analogy to type(of:), invocations of which are currently replaced by the compiler. But type(of:) behaves like a normal function in that you can wrap it in another function without changing its behavior. (If Swift had generic function values, you’d also be able to assign it to a variable.) And thanks to implicitly opened existentials, type(of:) can be implemented as a normal function in Swift 6, possibly wrapping a runtime call if source compatibility with Swift 5.x is desired.

Using a function-like spelling is also inconsistent with another very recent language change. There were significant parsing issues with any that could have been solved by requiring the use of parentheses or angle brackets, but those options were directly rejected. In fact, one reason for rejecting Any<> was that using such a familiar syntax might deceive a reader into thinking they could write their own equivalent of Any<>. Why reuse existing syntax for move(_:) when the same argument didn't hold for any?

@xwu proposed a sigil. I don’t think it could be a true operator for the same reason move(_:) isn’t a true function, and therefore I don’t think it’s a much better solution. Would the sigil reserve an otherwise-valid operator spelling?

The pursuit of alternatives

In my initial response, I said:

That specific phrasing carries a value judgment about move(_:) versus move-only types and implies an accusation of misprioritization, which was not my intent. What I really meant to convey was a suspicion that move-only types could be a functional superset of a move(_:) operation with less novel syntax, and thus it’s worth investigating them in tandem rather than deferring move-only types to future work. I’m happy to learn that @Michael_Gottesman is already hard at work on some alternatives/future directions.

I would like to propose another thread of investigation that is significantly closer to the existing proposal: move as a parameter modifier:

func takeAnArg(arg: move Int) -> Int { arg + 100 }

This shifts the syntax to the declaration, rather than the call site. Like inout, moving from a binding has effects on the caller’s environment, so a & would be required:

let x = 100
let y = takeAnArg(&x)
print(x) // error: value was moved out of 'x' and no new value was assigned

If we stop here, we have what we need to define move(_:) as a real function:

func move<T>(value: move T) -> T { value }

…which could either be provided via the Standard Library or left to clients to implement in the same way Swift leaves clients to implement local generic functions to implicitly open existentials.

Of course, this is an almost-bikeshed-level change to the original proposal, and does not address its interactions with inout or closure captures. But since there seems to be some supposition that explicit use of move(_:) will be rare to begin with, I would be interested to hear how the experts think this adjustment would play out in the cases where it’s currently necessary.

Extension to return position

A small extension to this design could address a related use case that move(_:) does not cover. Allowing move to decorate a return type could be defined as the spelling for placement-return:

// The ABI of `makeMeSomething` passes enough information
// to allow placement initialization of all of SomeHeavyStruct’s members.
func makeMeSomething() -> move SomeHeavyStruct {
  return SomeHeavyStruct(x: 100, y: 100, z: 100, w: 100, ...)
}

let ptr = UnsafeMutablePointer<SomeHeavyStruct>.allocate()
ptr.pointee = makeMeSomething()
// Avoids returning a temporary copy of SomeHeavyStruct via the stack.
// Runs the initializer directly on the storage pointed to by `ptr.pointee`.

Supporting move-only types

My understanding is that currently “no implicit copy” and “move-only” are being considered as separate primitive type attributes. My hope is that it would be possible to implement a move-only type by combining move-as-parameter-or-return-modifier with “no implicit copy”, but I haven’t yet succeeded.

9 Likes

That needs more explaining. The proposal isn't about introducing move-only types, nor is (openly) motivated by allowing move-only types. It's obviously related somewhat, but I'd hope move-only types can work without requiring an explicit move.

For instance, let's say I have a move-only type, it could work like this:

let x = MoveOnlyType()
let y = x // ok, implicitly ends the lifetime of x, moving to y
let z = x // error: x consumed by previous statement

You can use the move-only type without calling move() anywhere, because the value is implicitly moved for you whenever it's needed. If you don't write code that requires an implicit copy, you might not even realize you're dealing with a move-only type.


To stay in line with the above, a @noimplicitcopy modifier for variables could work like this:

@noimplicitcopy let x = SomeCopyableType()
let y = x // ok, moving to y, implicitly ends the lifetime of x
let z = x // error: x lifetime ended with previous statement

The remedy would be to make an explicit copy when assigning x to y:

@noimplicitcopy let x = SomeCopyableType()
let y = copy(x) // ok, explicitly copying to y
let z = x // ok, implicitly moved to z, x's lifetime ends here

No move() necessary in all of this. The only remaining reason to use move would be to explicitly end the lifetime early, hence why I'm suggesting it be called endLifetime().

2 Likes

Agree on the formalisation of __consuming (as well as __owned) in the language first. It is somewhat weird to have a formal proposal depending on some internal keywords inside the language, which makes it harder to understand and review (at least for me).

7 Likes

A big disadvantage of @noImplicitCopy let is that it requires you to rewrite all implicit copies of that variable into explicit copies just so you can change its last implicit copy to a move. This is potentially even more disruptive for something like a stored var property. If you want to move a value out of the property, you have to tag the property itself as @noImplicitCopy and then rewrite every single existing reference to it.

Whereas explicit move(_:) lets you confine the change.

1 Like

Any example where you don't care much about the implicit copies, except for the potential one at the very end?

Wouldn’t that be most of them? You’d reach for move(_:) when some profiling shows that your program is wasting time copying a value only to immediately copy it to another location and discard the intermediate copy.

You might be right about using move at the point of last use being preferable to the attribute approach. For the reason you mention, but also this:

I'm re-reading the first example of the motivation section and its counterpart with the added move. If at some point someone removes the line where a move is added, you might notice the removal of a move and decide ending the lifetime is still important even if you're no longer making the consumeFinalY function call. Whereas with the attribute approach the declaration is further away and it'd be easier to miss that a lifetime no longer ends where it should.

Is there a "manifesto" that explains how this all fits together? Obviously y'all have a pretty developed ideas of the various pieces and how they fit together, but that isn't apparent to me at least.

Manifestos are how we have traditionally bridged the gap between "wanting coherent design and common understanding of the overall goal" and "monotonic progress in small but careful steps".

-Chris

17 Likes

I asked the question how Move fits into the Ownership Manifesto here in the Pitch - and I still wonder, my question(s) where never really answered.

I would love to see that the Proposal brings up how this fits in Ownership Roadmap, maybe PR with updates to the Ownership Manifesto document? :)

1 Like

There's a recent ownership roadmap here: A roadmap for improving Swift performance predictability: ARC improvements and ownership control

2 Likes

Thanks, yes I’ve seen that one in December, which raises the next question, how does that relate to the original Ownership Manifesto? Does it make the original Manifesto Obsolete? Would be great to have one “source of truth” and also, Swift has been using markdown files in Github for its manifestatos for a long time, so why not update the Manifesto on Github?

4 Likes

I love the idea of something like move() — I’d definitely use it. For me a big draw would be the:

_ = move(x)

use...I’ve always wanted a language where I can say, “Ok, I’m officially done with this variable, you don’t need to worry about it past this point.” I think that’s an incredibly useful thing for the next person who reads my code.

I’m not super-interested in bike-shedding the exact spelling of move or whether this does everything anyone has ever dreamed of, and I don’t know why I’d care if this is a magic function or not. It seems really useful to me, so I’d like to have it.

-Wil

2 Likes

move only ends the lifetime of a specific movable binding. It is not tied to the lifetime of the value of the binding at the time of the move, or to any particular object instance. If we declare another local constant other with the same value of x , we can use that other binding after we end the lifetime of x

I am no expert but my mental model for move() was that it would sort of upgrade the binding to be move only.

var someA = move(someB)
var someC = someA // in my view someA should move to someC

The last line of that post says:

It would be perhaps useful to understand in what way the move function and other features from that post are necessary preconditions for move only types. Is the idea that move only types will somehow be built on top of the move function???

The dataflow analysis in SIL that detects uses after move is pretty much the same as what true move-only types will need to detect uses after being consumed. With move-only types, that consumption would be implicit when you pass off ownership of a value, but the underlying check will be the same.

2 Likes

I think it would strengthen the proposal if the motivation section explained how move(_:) contributes to the overall vision laid out by this roadmap.

3 Likes

The first paragraph of the roadmap says:

We're taking ideas from the ownership manifesto that are broadly applicable not only to move-only types but for providing finer control over high-level Swift code, and also using them as motivation to build the pieces we need to eventually support move-only types. The roadmap doesn't seek to supersede the ownership manifesto, but lay out the plan for how we intend to get there.

2 Likes

I’m generally in support of the proposal's scope. I too want to see __owned and __consuming formalized, but the roadmap indicates that’s in the offing, so I’m willing to wait for them. The same goes for move-only types and other such improvements. I want these things badly and I believe the authors when they tell me this is a good incremental step towards them.

I do feel like the move(_:) function is uncomfortable syntactically, but I don’t have a solid feeling about the right solution to that. So I’m going to ramble for a while and see where I end up.


Edit: You probably don’t actually need to read the rambly bit, so I’ve collapsed it

My first thought was that, since the argument to move(_:) is effectively modified (by ending its lifetime), it ought to be marked with &:

let y = move(&x)
consumeFinalY(move(&y))

This naïvely means that move(_:) should be <T>(inout T) -> T, not (__owned T) -> T, although that’s kind of an awkward fit since it leaves the argument uninitialized.

On the other hand, __owned actually has the same problem. This led me to think that perhaps the parameter to move(_:) should not be inout or __owned, but some new modifier—call it __moved as a strawman—that requires & and enforces the rules we want for arguments to move(_:). That would keep move(_:) from, say, being assigned into a variable for an <T>(__owned T) -> T function, and would perhaps allow other functions to use __moved to get the same behavior:

func consumeFinalY(_ y: __moved [Int]) { ... }
consumeFinalY(&y)

(This is basically the same as one of @ksluder’s suggestions above.)

But then I realized, do we actually want other functions using __moved? Or do we want functions to use __owned parameters and leave it to their callers to write move(_:), so they can choose whether they want moving or copying semantics on a per-call basis? On reflection, I think we want the latter—it’s more flexible and more explicit.


If so, I think I need to change my mental model of the situation: The move(_:) function itself is supposed to be the &-style marker of the unusual behavior being applied to the argument; the problem I’m having is that it looks so much like an ordinary function that my brain doesn’t flag it as marking unusual behavior.

I think that’s what @xwu is getting at when he suggests using a prefix-operator-style marker, like <-, to indicate the move. The problem I see with this is that, unlike &, <- is going to be very rare, so I’m worried that learning materials will skip over it and then people won’t know what it means when they see it in their code. Perhaps a different sigil would at least hint at the behavior more clearly (&&, perhaps?*), but something that involves a searchable word might be better.

* “You’re just copying && from C++!”, I hear you cry, but this is more a case of convergent evolution. In both C++ and this hypothetical Swift use, && basically indicates “kinda like &, but a little different and weirder”, but they would be used in very different places (call sites vs. parameter declarations).


That leads me to the keyword-style suggestions, like @move, #move(...), or just move.

I have to admit that I don’t like either of the sigiled forms. @ and # don’t suggest side effects to me; @ has never been applied to expressions and has always had an “adjective” feel that seems inappropriate here, while # generally has a "macro-like" connotation of being a shorthand for something you could have written out instead (although that’s not strictly true for #available). Neither one really feels appropriate for this.

That leaves us with plain move:

let y = move x
consumeFinalY(move y)

I like that contextual-keyword move is clearly not a normal function, but some kind of special language feature, much like try. It looks like something you haven’t seen before and should probably look up, and since it’s a word, you have a reasonable chance of looking it up successfully. But I am sensitive to the parsing concerns. I wonder if they could be addressed by also requiring an & on the operand:

let y = move &x
consumeFinalY(move &y)

The parser would then know that a move not followed by & is just the identifier move, so things like move(x) would not need to be explicitly banned.

But that does still leave the issue that it feels like overkill to design totally custom expression syntax for a niche feature. It doesn’t feel great to dedicate so much effort to something that won’t be used very often. (And realistically, to dedicate similar effort to similar things later in the roadmap, like the explicit copy operator we’ll need for @noImplicitCopy.) But maybe it’s the right thing to do anyway.


So I suppose where I end up is here: I think there are good ergonomic arguments for using a contextual keyword (perhaps with &), but I’m sympathetic to the notion that a contextual keyword would be too much effort for a feature so niche and we should just use a function (perhaps with &) and let the ergonomics suffer a little. I think the other ideas I’ve considered here fall into awkward spots where they’re too much effort, or too little ergonomic gain, or both.

9 Likes