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

(x + y) doesn't have a binding, so move (x + y) wouldn't mean anything. Requiring parens might be reasonable, but parsing as move (x + y) seems nonsensical.

As a side note, I don't really want to get into bikeshedding, but I think I'd prefer to call this operation unbind or unlet. move is something of a term-of-art, but it's a bad name and leads to all sorts of confusion, as this thread has shown.

15 Likes
Bikeshed

unlet raises the specter of unvar.

Python uses del, if we want a familiar word that also happens to be 3 letters.

even this:

move x+y

Maybe just prohibit it?

"unlet" is good, just note that in the pitch, this is allowed:

let x = ...
move x
x = ...

while "unlet" word suggests the last "x = ..." is an error, and it should be written as:

let x = ...
unlet x
let x = ...

In other words "unlet" word suggests returning to "stage 0" while the pitch "move" suggests returning to "stage 1":

// stage 0
let x: Int
// stage 1
x = 1
// stage 2
1 Like

This revision is much betterā€”my remaining comments are almost nitpicks.


Iā€™m not sure I like this. When I imagine looking at this code without any foreknowledge of move:

func f(_ x: SomeClassType) {
     move x
     useX(x) // !! Error! Use of x after move
 }

I think I would find move x completely mystifying. Is it moving a file? Is it animating something? Is it transferring an object between concurrency domains? Is it migrating a distributed actor to the current node? Does it do something specific to SomeClassType?

I think requiring the discarding assignment helps clarify move a little because it at least suggests that the usual nature of move is to produce a value, and we are simply ignoring that value in this case. Butā€”since bikeshedding has now come upā€”a different name for move might also help clarify the issue. For instance, I think any of these would be more understandable than move without the discarding assignment:

    take x
    transferownership x
    handover x
    consume x

    useX(x) // !! Error! Use of x after <whatever>

(To be clear, Iā€™m not trying to argue for these specific namesā€”just demonstrate that some name change might resolve this issue.)


move x.y.z // Syntactically OK (although x.y.z is not currently a movable binding)
move x[0] // Syntactically OK (although x[0] is not currently a movable binding)
move x + y // Parses as (move x) + y

I assume you plan to achieve this by adding a new production to prefix-expression thatā€™s analogous to the one for in-out-expression. Should we amend a description of that into the proposal?

9 Likes

It's important to me that the semantics of the move intrinsic are identical the semantics of "consuming" any move-only value. Whatever behavior we specify won't be specific to move. I'm also saying that this optimization is not only silly but harmful even when we don't have move-only values. So, we'd want to say something about the side effects of potential object deinitialization in general.

I would also like to formally specify away the bad optimization. But I don't think we can invent the formal language that we need in this proposal. We need a separate "deinitialization" semantics proposal. What I really don't want to do is make some broad statement about "side effects" that is specific to the move intrinsic.

At any rate, this is great feedback.

6 Likes

Okay, this helps crystallize things a bit more for me.

This also makes sense, though I wonder if it is in conflict with the proposal text. The proposal raises the point several times that move serves not only as a tool useful against modifications of the source, but also against optimizer behavior and future language implementation changes. If we don't have the formal language to guarantee that move protects against the language implementation in this way, perhaps it would be appropriate to specify move for now as a source-level guard, and then further specify the semantics once we flesh out the initialization semantics?


Even as just a source-level guard, though, I think the potential for this 'harmful' optimization speaks to a broader discomfort that I've been feeling but haven't been able to put into words until just now. Again returning to the motivating example for move:

func test() {
  var x: [Int] = getArray()
  x.append(5)
  
  var y = x
  longAlgorithmUsing(&y)
  consumeFinalY(move y)

  x.append(7)
}

the proposal sells move as a solution to the problem of "what if someone tries to use y after x.append(7) and accidentally causes an implicit copy"? But that problem is indexed on two bindings, x and y, and the solution is unilateral: it only affects usage of the y binding. Nothing stops an accidental insertion of an x.append(6) within the 'critical' section between the y = x alias and the final move of y, just like the potential 'optimization' discussed before:

var y = x
longAlgorithmUsing(&y)
x.append(6) // oopsie
consumeFinalY(move y)

It seems like the robust solution to the problem posed starts looking more like a Rust-y lifetime system:

var y = strawmanDontUseTheseAtTheSameTimeKeyword x
longAlgorithmUsing(&y)
x.append(6) // error: 'x' cannot be modified while 'y' is still alive (used below)
consumeFinalY(y) // OR error: 'y' used after lifetime implicitly ended (by use of 'x' above)

x.append(7) // OK, lifetime of 'y' implicitly ended here

I haven't really thought this through, but it does seem like it would be better if we could properly express the relationship we're worried about rather than relying on ending the lifetime of a single binding.

2 Likes

Looks like you've just reinvented the roadmap's "borrow variables":

2 Likes

I think it would be slightly different, since in the motivating example for move x and y are semantically independent copies rather than one being a 'borrow' of another, but the relationship being expressed is very similar, yeah.

One more bikeshed following up which I think would feel natural if approaching swift without obj-c mental state:

release x

3 Likes

I really like the nesting idea due to it falling neatly into the bracket defined scope usage, something which always made clear sense to me.

2 Likes

disown x ?

I agree with @beccadax (and @ksluder up-thread) that I actually think this has made the thing I was objecting to worse. I thought that _ = move(x) as a synonym for drop was inscrutable, but move x is definitely worse.

The following question is asked in a very tongue-in-cheek way but...are we avoiding adding another word for a reason? I feel as though there has been unstated resistance to using the word drop, or indeed any word other than move, though these revisions: many folks have said they don't love the spellings, but I don't recall seeing an articulation of why we should choose to continue to use the word move here.

Anyway, if we must use the word move then _ = move x is a clearer spelling, but I still think neither of these are better than simply using another word.

17 Likes

+1 to use drop as a keyword instead of move.

3 Likes

var y = move(x)

is it dropping or moving ?

+1 I agree with this.

2 Likes

If this feature is to be an advance feature I think having a contextual short keyword should not be a goal. what if instead of a keyword we had a block?

I havenā€™t seen it discussed in this thread but I thought that having a block where the semantics change from implicit copy to always move would be nice

movedefault {

var a = b // b moved to a

var c = b // // b moved to c

// after this block variables are dropped unless they were passed to other functions. 
}

I think youā€™re right that the proposal oversells the future-proofing that move(_:) enables. The example reads to me as an example of discovering and fixing an accidental COW. The closest ObjC analogy I can think of is something like this:

id x = ā€¦;
{
  id y = x;
  // Fixes bug#1234 where we accidentally used x during this loop
  #define x __use_y_instead__
  while (y != nil) {
    [y doSomething];
    y = [y next];
  }
  #undef x
}

I think you can solve this particular problem by moving x into a move-only wrapper, which I think is friendlier than Rustā€™s lifetime system. You do need a way to borrow the value out of the wrapper, which isnā€™t really in scope for this proposal. (A borrow is essentially a move with a static guarantee of moving back into its original binding.)

2 Likes

[bikeshed] "release", "consume" and "take" all make sense. To make a choice, we need to see into a future where we have matching parameter type, function return type, and variable type modifier keywords. We've been gravitating toward using "borrow" for non-owned immutable things. If we get something without borrowing it, then we've obviously "taken" it.

// Forward ownership of a transformed value.
func transform<T>(_ input: take T) -> T { return input }

// transform 'x' without copying it
let y = transform(take x)

To either take or borrow something without coping, the call site needs an explicit keyword. That keyword needs to be consistent with the parameter convention. The parameter convention has some sensible default, and explicitly modifying the parameter type only changes that convention. The parameter convention does not actually force the caller to take or borrow its argument!

See how "take" contrasts with "borrow" here:

// By default, a callee receives a borrowed argument. Using borrow here is redundant.
func read<T>(_ input: /*borrow*/ T) -> T {
  // Passing 'input' to an argument will *copy by default* unless you explicitly "borrow"
  // (otherwise it's up to the optimizer to remove the copy!):
  print(borrow input)
}

[EDIT] Passing input as an argument to print above should actually borrow by default, even at -Onone because it is immutable. (pass-by-value and pass-by-borrow are semantically identical for immutable values). When the argument is a 'var', however, using the borrow keyword at the call site is needed to guarantee that the compiler omits the copy.

In a future world, we may be able to do awesome things like borrow aggregates:

extension Optional where Wrapped: BitwiseBorrowable {
  // Construct a borrowed value whose lifetime depends on the borrowed arguments.
  borrow public init(_ some: borrow Wrapped) { self = .some(some) }
}

Or borrow big values without copying them:

struct Container {
  var elements: (Int, Int, Int, Int) // imagine something bigger
}
let container = Container(elements: (0, 1, 2, 3))

// borrow the same large tuple as much as you want without copying it
borrow elements1 = container.elements
borrow elements2 = container.elements
6 Likes

Personally if we are going to use a different word, I would prefer the term take. The reason why is take is similar to move and suggests that the value is returned. release has too much of the baggage of ARC. Consume to me doesn't suggest that a value is returned.

8 Likes

Seems to me that we should not commit to naming until we have more of the borrowing story fleshed out. I wonder if we could introduce _move as a placeholder knowing that this will change in future versions. Sort of reminds me of _modify/yield which folks can use but not fully supported.

5 Likes