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

No. __owned is a property of the parameter, and you cannot imply it by something you write on the call site, just like you cannot make a function that takes an Int suddenly accept a String by writing something on the call site.

Sorry, this thread of discussion started with the question of whether __owned already sufficed. So my follow-up question was in the context of the variant in which move lives on the parameter declaration, which has since been suggested anew.

Forgetting about that variant, though: if move x at the call site does not imply passing the argument as if the parameter were declared __owned, does that mean move doesn’t avoid ARC traffic when calling resilient functions? Or does the guaranteed ABI make it possible to move the argument into the parameter with zero ARC traffic?

usually you would not _ = move self in the case block, you would do something like:

switch move self 
{
    ...
}

for an array payload, it does not make a huge difference, but some payloads can be considerably more complex, and do not have a natural “empty” state.

2 Likes

A function that takes a borrowed (guaranteed) parameter is never going to be able to truly accept a move of an argument value. The caller can "move" the value into a temporary and thus end its lifetime immediately after the call, but if the callee needs an owned value, it will need to copy the argument and thus incur e.g. RC overhead. The owned vs. borrowed distinction is important to get right for optimal performance; with move-only types, it's equally important to get right for basic correctness, and we'll probably force people to be explicit.

Ownedness is part of the ABI signature of the function, so you can make a resilient function with an owned parameter. But if you have an existing non-owned parameter, you're stuck with it; making it owned would be an ABI break.

1 Like

This leaves me a little perplexed. This feature is being positioned as a performance tool, and using move for arguments at the call site has been a major focus of discussion. But it sounds like the major performance benefit (minimizing copies and ARC traffic) silently doesn’t happen if you’re calling a resilient function?

It has, but I don’t think it’s addressed in the updated pitch. Does this pitch intend to include the option of move decorating a function parameter in the definition, or is move only for the caller?

Argument position = at the call site. Parameter position = at the declaration.

Though I’ll just go back and use “call site”.

I hope my question is still addressed, despite being raised in confusion. Can a function parameter be defined to be move like one can be defined inout?

right now, _move(_:) cannot be used in a computed property or subscript, so this still isn’t possible to implement. i’m running into a similar issue, where i am trying to make the following “Tree” view of an underlying Forest<Value> buffer be mutable:

extension Forest 
{
    @frozen public 
    struct Tree
    {
        public 
        var forest:Forest<Value>
        public 
        var first:Index 

        @inlinable public 
        init(forest:Forest<Value>, first:Index)
        {
            self.forest = forest 
            self.first = first 
        }
    }

    @inlinable public 
    subscript(head index:Index) -> Tree 
    {
        _read 
        {
            yield .init(forest: self, first: index)
        }
        _modify 
        {
            var tree:Tree = .init(forest: _move(self), first: index)
            yield &tree 
            self = tree.forest 
        }
    }
}
Insert.swift:27:9: error: 'self' used after being moved
        _modify 
        ^
Insert.swift:29:43: note: move here
            var tree:Tree = .init(forest: _move(self), first: index)
                                          ^
Insert.swift:30:13: note: use here
            yield &tree 
            ^

I tried to cover this in the second paragraph of my response, but I can elaborate.

Being able to explicitly cut short the lifetime of a local value avoids copies in two major ways:

  • It allows the programmer to forward ownership of the value directly into something that naturally needs ownership, like an owned parameter or an assignment. This eliminates a copy of the value. This copy can also be potentially eliminated by the Swift optimizer, but being able to guarantee that it will happen is still valuable; for example:
    • being explicit about things that are important is generally good,
    • the optimizer will always be imperfect, and
    • we are considering rules that will specifically tie the optimizer's hands in some cases (in exchange for more reliable semantics around destruction order and so on).
  • Even if a direct copy is not avoided, it allows the programmer to reduce the number of outstanding copies at a given point in the program, which can avoid copies of buffers and other values due to the dynamic copy-on-write optimization.

Focusing in on one piece of that first point, there are two ways we can forward ownership value into a parameter during a call.

  • The optimizer can analyze the callee to figure out that it wants ownership of the value, alter it to take an owned parameter, and then alter the call to forward the value. This is inherently a complex analysis that is therefore not going to happen reliably. It is also blocked by any sort of polymorphism in the call, including the "deployment polymorphism" of a resilient call, but also protocol dispatch, class dispatch, and all other kinds of indirect call. That is the only way in which resilience enters into this discussion.
  • The function can just explicitly say that it wants ownership of the value. Today, the primary way to do this is with the unofficial __owned feature. This works through any kind of polymorphism, including resilience, as long as the call site knows about it. (For example, to make it work for protocol dispatch, both the concrete implementation and the protocol requirement must say that the parameter is owned.)
7 Likes

Thanks, that does clarify things considerably.

I completely agree with this.

However, I am concerned that being able to type foo(move x) when the argument to foo doesn’t actually support moving is dangerous. It feels like inline in C, which is notoriously unreliable. I would probably want the compiler to reject foo(move x) unless the parameter was statically known to be __owned (or consuming). Which is isomorphic to my suggested alternative of making move a parameter modifier instead of a call-site keyword.

edit I guess it’s not quite isomorphic. Swift already assumes __owned and consuming in some places, and it’s probably necessary to allow users to choose whether to move or copy into such arguments. So move would become a superset of __owned that mandates its argument be moved, my preferred syntax for which would be calling a function that returns a move T return type.

It sounds like you’d like move decorating the parameter at the call site to be analogous to the & for calling inout parameters, requiring the programmer to acknowledge they understand the ramifications of sending that parameter to that function. Is there anything like that now for the experimental features (__owned, etc.)?

I wonder if it would help to align the naming of the __owned parameter modifier and the move operator name. I like take as a name for both:

func consume(x: take Foo) {}

func makeAFooAndGiveItAway() {
  let foo = Foo()
  doStuffWithFoo(foo)
  consume(take foo)
}

Another operator we've been considering is an explicit borrow operator, which would be a way to force passing an argument by borrow without copying it. Particularly when accessing class instance variables, globals, and other shared state, we will normally copy when they are passed as arguments to avoid imposing an exclusivity requirement on shared mutable state longer than we have to, but this is not always desirable:

class Foo {
  var bar: Bar
}

func useBar(_: Bar) {}

func useFooBar(_ foo: Foo) {
  // Will normally defensively retain/release foo.bar
  useBar(foo.bar)

  // Suppress the copy and pass the reference to foo.bar directly
  useBar(borrow foo.bar)
}

Since that modifier aligns with the behavior of what we currently call __shared parameters, borrow also makes a nice name for explicitly labeling pass by borrow parameters:

func useBar(_: borrow Bar)

Then you can say that, in order to guarantee optimal no-copy behavior, you use the matching move or borrow modifier on the call site argument and the function definition.

11 Likes

It feels like the third line should be consume(give foo), since consume is receiving the result of the expression.

I’m very excited about borrowing. But how does borrow Bar differ from inout Bar? Is it a good idea to keep the familiar &foo.bar syntax at the call site?

I’m sorry to keep harping on this, but I would really like to hear some feedback from someone on the language workgroup about func move<T>(_: move T) -> move T. I appreciate that the revised proposal directly responded to feedback about the pseudo-function syntax, but it feels like this new form of expression is actually going to wind up in many places, including places where very different syntax is already used for extremely similar effects.

Does anyone on the language workgroup have positive or negative opinions about going the other direction, making move(_:) and friends real functions and reusing as much familiar syntax as possible?

I’m very curious about this as well. I have no experience with __owned or __shared function parameters now. Can one call them now with undecorated variables the same way you’d pass to a “vanilla” function parameter or must one decorate the passed variable with & as one does for inout parameters?

. sink seems to also be used for the same purpose in val-lang. not sure if sink makes sense for swift but I do like the idea of using the same keyword from both parameter and using site.

1 Like

A brief response to put the ByteBuffer[View] digression to rest.

To be clear, this pair already exists. We have a ByteBufferView.init(_:) that takes a ByteBuffer and is isomorphic to .readableBytesView, and we have a ByteBuffer.init(_:) that takes a ByteBufferView. Indeed, a goal of ours for some time has been to make it "zero cost" to jump between these two representations, as that allows us to produce a cheap way to operate on a ByteBuffer when you need a collection (as ByteBuffer isn't one).

In our case, then, what we want is explicitly to transform between these two cases. The initializers exist already so the flows back and forth are well-defined, they just happen to inevitably trigger CoW in the current Swift language.

That's ok, we have initializer pairs as discussed above, which forces a bit of cognitive burden on the developer, but I think a manageable amount.

More broadly, I think it's not clear exactly how move should function on self in a _modify coroutine.

1 Like

Fixing the mandatory COW on the initializers doesn’t address the existing .readableBytesView getter. How would you decorate that so the lifetime of the ByteBuffer ends upon return from the getter?

Forcing the API designer to abandon getters for wrapping initializers would be an unfortunate regression in the language’s expressive capabilities, especially because it would foreclose the ability to use opaque types for things like .readableBytesView.

1 Like

As I understand it, borrows would be immutable. So I would assume that we would not need the & sigil for the same reason we don't need it when passing a variable to an immutable pointer parameter.

__owned and __shared don't change anything at the call site. In fact, you've already used them even if you haven't used the experimental keywords. Parameters to initializers (and newValue for setters) are __owned by default. All other function parameters default to __shared.

1 Like

To be clear, initializers are the preferred way of spelling conversion between types as per Swift’s opinionated API guidelines. The point is well taken regarding opaque types. However, it bears mentioning that having a solution that avoids COW when spelled in the form of initializers is not a retreat from or abandonment of current practice but in fact reinforces the overall design direction of the language.

4 Likes