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

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