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

Hello, Swift community!

The review of SE-0366: Move Function + "Use After Move" Diagnostic begins now and runs through August 8th, 2022.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager by email or DM. When contacting the review manager directly, please keep the proposal link at the top of the message and put "SE-0366" in the subject line.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at:

https://github.com/apple/swift-evolution/blob/main/process.md

Thank you,

Holly Borla
Review Manager

23 Likes

I'm broadly +0.5 on this proposal—I support the addition of the move functionality at a high level but I remain apprehensive about a couple items that I raised in the pitch: namely, move(_:) as a function specifically and the lack of @discardableResult and/or drop.


The proposal introduces move as the following:

The move function consumes a movable binding, which is either an unescaped local let, unescaped local var, or function argument, with no property wrappers or get/set/read/modify/etc. accessors applied.

but IMO this is glossed over far too quickly—the concept of a "function which consumes a movable binding" (or indeed, any binding at all) is an entirely new concept, and isn't even representable in Swift today. The purported signature of the move(_:) function is a bit of a fiction. This is discussed and justified in Alternatives considered:

Declaring move as a function also minimizes the potential impact to the language syntax. We've introduced new contextual keywords without breaking compatibility before, like some and any for types. But to do so, we've had to impose constraints on their use, such as not allowing the constraints modified by some or any to be parenthesized to avoid the result looking like a function call. Although that would be acceptable for move given its current constraints, it might be premature to assume we won't expand the capabilities of move to include more expression forms.

AFAICT, though, this really only rebuts the proposed spelling of move x and not the other alternatives discussed just above such as #move(x) or @move x. In a vacuum it strikes me as equally premature to assume that move will expand to include more expression forms—the expansions discussed in Future directions still only really cover expansions of the potential bindings we could apply this function to.

As written the proposal doesn't convince me that move(_:) will ever be able to be "just another function"—not even the Ownership roadmap lays out such a scheme. If it were the plan of record that we'd be adding ownership features until move(_:) could be a function that anyone could write, I'd be a lot more on board with this direction, but since that's not the case I can't really get past the feeling that move-as-a-function isn't appropriate for Swift. There are too many limitations and special cases for not enough gain, IMO.

It's not really enough to hinder my overall support for the proposal, but I think that in its current form it will become a part of the language that we will forever have to caveat as "yes, I know it looks like a function but it's better not to think of move(_:) like a Swift function at all because X, Y, Z."


My second objection is less pressing since it could be rectified in a backwards-compatible manner, but I still think it would be nice to address as part of the initial proposal.

I also want to reiterate my thoughts on the lack of @discardableResult in this proposal, which is discussed as part of the rejection of the drop function:

We could also introduce a separate drop function like languages like Rust does that doesn't have a result like move does. We decided not to go with this since in Swift the idiomatic way to throw away a value is to assign to _ implying that the idiomatic way to write drop would be:

_ = move(x)

suggesting adding an additional API would not be idiomatic. We do not propose making move use the @discardableResult attribute, so that this kind of standalone drop is syntactically explicit in client code.

This reasoning still strikes me as somewhat backwards (in the formal sense). The way I see it is that using @discardableResult is the idiomatic way to design an API for which one of the expected uses is to discard the result. If that's the case for move(_:) then we should have move(_:) be tagged as @discardableResult. If we're worried that a bare move(x) is somehow not explicit enough (see below), that's justification for a drop function.

The idiomatic way to throw a value away is to assign to _ only in cases where the API designer does not consider discarding the result a 'supported' use case of the API—it doesn't make sense to me to design an API where we 'support` the drop functionality by forcing users to use discard assignment.

I also have this lingering related question from the pitch:

23 Likes

Overall I am in favour of this pitch and it does look simpler than the corresponding Rust feature (although I must admit my Rust knowledge is minimal and maybe there's something more to their way of doing things).

Edit: changed my opinion on this pitch until I see a good example that shows the superiority of this approach to alternatives like nesting / lexical scopes.

A couple of questions:

  • AFAIK "move" semantic is possible today, albeit in a less nice and less explicit form. Is this correct observation? See examples below.

Example 1:

let x = "1"
use(x) // "1"
do {
	let y = x
	use(y) // ok
} // y lifetime ended
use(x) // ok
use(y) // error

Example 2 (a forehead warning: quite ugly):

let x = "1"
print(x) // "1"
guard let x = false as Bool? else { fatalError() } // previous x lifetime ended
print(x) // false

I understand no one in their right mind would want to write "Example 2", but compared to example 1 this pitch allows to write essentially the same but without the need of extra brackets / nesting. Is this correct observation?

This deserves a word or two of clarification:

  • will that work with "let x" instead of "var x"?
  • can I reassign x to an expression of a different type?
  • why is this feature needed?

I like drop and while I can tolerate that it is not available out of the box this raises a question: will I be able implement it myself? Is this correct implementation:

func drop<T>(_ value: __owned T) {
	_ = move(value)
}

+1 for the pitch overall.
Edit: see the Edit above.

4 Likes

I'm also +0.5 on this. In general the feature looks good, but I do hope to see the formalisation of the __consuming annotation along with the resolution of some of the outstanding bugs in that area for SwiftPM packages. Additionally, I think it'd be good to add drop(): I just fundamentally don't find the argument that _ = is the idiomatic Swift spelling for throwing something away to be all that compelling, and _ = move(x) is a very indirect way of spelling that idea.

21 Likes

Bikeshed follows:

Assuming that a function spelling is correct, is anyone else concerned about using such a short, basic, name for this (relatively niche) operation? move is a very basic name for an operation that seems quite advanced.

As a free function, 'move' will appear to programmers a lot — like in autocomplete — and newcomers might be led astray by such an unassuming basic name as 'move'. It might also just cause some awkward clashes in the top-level namespace. For just one real-world example, the Swift Playgrounds tutorials introduce a function named move quite early on (to control the on-screen character). SwiftUI already has two 'move' basename members.

Whereas, something like moveLifetime feels a lot clearer about what it does and the slightly longer name is more commensurate to its frequency of use.

All that said, I'm not staunchly against move. But it doesn't intuitively feel quite right as a function name.

19 Likes

Is “move” a term of art? It seems an odd choice to me. The functionality feels more like a “finalize” or “forget” or something else that sounds like an end.

2 Likes

-1 on move(_:) as a function. Wholeheartedly +1 on move semantics and use-after-move diagnostics.

My initial opposition to move(_:) as a function is well expressed by the first sentence in the Alternatives Considered section:

However, my opinion has evolved a little bit and now I think it would be worth trying to avoid any syntactic marker at the site of the move.

Yes.

No. Swift has hidden retain and release since its inception. Requiring the use of an explicit move(_:) function feels like a step backward.

I would much prefer the language team focus first on move-only types as alluded to under the “Suppress Implicit Copying” heading of the Alternatives Considered section. Move-only types are akin to non-Sendable types in that both dictate when reads are permitted. They just have opposite defaults—types are not Sendable by default, but they are copyable (that is, not move-only) by default. Move-only types could be signified with conformance to a MoveOnly marker protocol:

struct Pair : MoveOnly {
  var x: Int
  var y: Int
}

let p1 = Pair(x: 100, y: 100)
var p2 = p1 // note: value moved to `p2` here
print(p1.x) // error: value has been moved from `p1`

Like Sendable, we can imagine allowing developers to write move-only “envelope” types to opt values of copyable type into move-only semantics. Such an envelope would subsume the need for a standalone move(_:) function.

class SomeClass { }
struct MoveOnlyRef<T: AnyObject>: MoveOnly {
  private var wrappedValue: T?
  init(_ wrappedValue: T) {
    self.wrappedValue = wrappedValue
  }

  @_semantics(move)
  mutating func move() -> T? {
    // This implementation wouldn’t actually be used,
    // since this method is marked with @_semantics.
    // But it helps illustrate the value-level effects.
    let tmp = wrappedValue
    wrappedValue = nil
    return tmp
  }
}

let ref1 = MoveOnlyRef(SomeClass())
let ref2 = ref1.move()
print(ref1) // Optional<SomeClass>.none

C++ has had std::move since C++11, and it’s kind of a nightmare. It’s viral and interacts poorly with standard optimizations (NRVO). I fear move(_:) will be taught as "std::move() for Swift", and worse that it might in fact share enough of its confusing semantics. (Indeed, the proposal even refers to C++ and Rust’s use of move as a term of art.)

A quick reading and periodic discussion with the authors over the course of development of this proposal.

11 Likes

A further question I have: does move present a reordering barrier to the optimizer? The proposal says that in this example:

func test() {
  var x: [Int] = getArray()

  x.append(5)
  
  var y = x
  longAlgorithmUsing(&y)
  consumeFinalY(move(y))

  x.append(7)
}

there's a 'guarantee' that x is unique at the point of x.append(7). But what stops the optimizer from reordering here so that the actual execution order is closer to:

this example:

func test() {
  var x: [Int] = getArray()

  x.append(5)
  
  var y = x
  longAlgorithmUsing(&y)
  x.append(7) // copy here?
  consumeFinalY(move(y))
}

?

1 Like

I have to agree with Kyle that this doesn’t feel “Swifty.”

3 Likes

Since the implementation of Array.append() is known to the compiler, I would expect x.append(7) to be eliminated as a dead store and thus be unaffected by the presence of move(_:)

1 Like

I'm not sure "this should just be implicit" is a very convincing argument here. I mean, sure, it's always best if the compiler can just optimize something for you automatically. However, part of the idea behind this proposal is that it is sometimes important to have an explicit tool to force an optimization, even if the language can also do it implicitly. And that would be true even if the optimizer weren't practically limited by the need to not break common, established idioms — which it very much is.

There's also a future direction in this area to move out of a mutable optional, which wouldn't require the value to be stored in a local variable. I would expect that we would spell that similarly, and so there ought to be a common established language for expressing these things. But of course that's a future direction — maybe there's a good reason to give that a different name.

14 Likes

Okay, sure, but that resolution calls the entire motivating example into question. If x.append(7) can be eliminated why do we care about ‘uniqueness’ there at all? I’m taking it as given that the motivating example would actually cause the copy we’re trying to avoid by using move.

And it doesn’t seem to me that the resolution can be something in the vein of ‘the optimizer is smart enough not to do that’ because part of the motivation of move is to allow us to reason about the behavior of code regardless of optimizer behavior.

You’re saying we could have move(_:) for explicit purposes and still have a more Swifty implementation (that likely desugars to move calls) that implicitly applies move-only semantics (and errors accordingly) for MoveOnly conforming types? I’d hope it is not too long between releases when it’s crufty and when it’s Swifty.

No. I am saying that it is sometimes useful (even important) to have a tool that forces the implementation to move a value and diagnose if that's impossible. The optimizer can already move values in some cases, which would make the use of that tool redundant in those cases, except for getting the diagnostic if the code changes to take the optimizer out of that case. In the future, we can hope there will be more such cases. But it will always be useful to have that tool.

Also, for what it's worth, MoveOnly would be a negative type constraint, which is not how we generally model or think about these things. A move-only type is one that does not (or at least is not statically known to) satisfy the Copyable constraint, which all types currently do.

5 Likes

It might be out of scope here, but I don’t know where else I would ask: Given that, how would we move forward into a world where some types are not Copyable if it’s current implicitly on every type?

There's a discussion about this topic over here: Move-only types and Any

3 Likes

I’m not sure that move(_:) as a function provides such a tool. For example, the semantics of inout are defined in terms of a temporary copy and write-back. There have been requests (from myself and others) for a way to statically guarantee in-place mutation. Does move(_:) as a function achieve that?

Meanwhile, encoding move-only-ness in the type makes it easier to extend the definition of inout to guarantee in-place modification of move-only values while retaining the current combination of formal semantics and best-effort optimization that applies to copyable types.

But in a world with Copyable, either everyone would have to write : Copyable to get the current Swift behavior, or there would need to be a new syntax for negated type constraints (e.g. : !Copyable).

This all goes toward the question of whether move(_:) feels “Swifty”. Swift definitely has a history of leaving things unwritten, especially behaviors that are guaranteed by the language. The behavior of : MoveOnly can have guaranteed effects at the use site without requiring explicit spelling unless a programmer wants to opt in to a using move-only envelope.

I went back to double-check the design, and I do think there’s a hole:

let variable1 = 100
let closure = { print(variable1) }
if Bool.random() {
  let variable2: Int = move(variable1)
  closure()
} else {
  closure()
  let variable2: Int = move(variable1)
}

variable1 meets the definition of “movable binding” according to the proposal, but clearly one of these codepaths is ill-formed. Unlike the examples in the proposal, variable1 is captured, not referenced in an argument.

Does the compiler “tag” the value assigned to closure with the fact that it captures variable1, and reject the true branch of the if? Or does it permit the capture and blow up at runtime in the case where move(_:) is invoked before closure()? (I believe this is undefined behavior in C++, but in Swift it would probably require runtime bookkeeping.)

If the move-only-ness of variable1 were encoded in its type, the compiler could statically reject the capture of variable1.

1 Like

Wouldn’t closure here be implicitly @escaping (even though it doesn’t actually ‘escape’) thereby making variable1 non-movable?

I’m not seeing why it would be “implicitly @escaping.” It doesn’t escape its enclosing scope.

I suppose @escaping could be the “tag” I mentioned, but the compiler would then need to retroactively add that “tag” once it encountered a move of a captured variable.