Why is `self` considered used-after-consume in coroutine accessors?

I used the following code to implement a wrapper type. However the compiler complained "'self' used after consume":

import Foundation

public struct Wrapper<T: ~Copyable>: ~Copyable {

    private var core: T

    @usableFromInline
    init(_ core: consuming T) {
        self.core = core
    }

    @usableFromInline
    consuming func get() -> T {
        return core
    }
}

public extension Data {
    var ext: Wrapper<Data> {
        @inlinable
        @inline(__always)
        _read {
            yield Wrapper(self)
        }

        @inlinable
        @inline(__always)
        _modify {
            var wrapper = Wrapper(consume self)
//            self = Data()
            yield &wrapper  // <- error: : 'self' used after consume
            self = wrapper.get()
        }
    }
}

Currently, I can workaround this issue by uncommenting that self = Data() line, however I'm confused why that's necessary.

This is my thoughts: when the caller start calling this _modify accessor, the caller has already gained exclusive ownership of self, and during the period of yielding &wrapper, Swift's Law of Exclusivity should forbid any overlapping references to self. So it should be safe to leave self in a consumed state during that suspension period, we just need to remember reassigning a new value to self before the accessor fully returns.

Would be glad to hear thoughts from ownership maniacs.

1 Like

Keep in mind that the part after yield isn't run if the caller throws, so in that case you could end up not reassigning to self. I wonder if using defer instead of writing code after yield would work better?

2 Likes

I finally managed to make the code work by using a defer statement, though I had to drop the consuming modifier on Wrapper.get().

If I didn't do that, there would be a different error, which also confused me:

_modify {
    var wrapper = Wrapper(consume self)  // <- missing reinitialization of closure capture 'wrapper' after consume
    defer {
        self = wrapper.get()
    }

    yield &wrapper
}

But regarding this:

the proposal of coroutine accessors says differently: "... Then the code after the yield executes", I wonder which is correct.

1 Like

Yeah, _modify has existed since long before that proposal and has slightly different semantics from its yielding mutate.

2 Likes

It's a limitation of the current implementation, documented in "Noncopyable variables captured by escaping closures" section of SE-0390. The following tests use consuming parameter, instead of non-copyable value, for simplicity. Test 1 is similar to the example in SE-0399. Test 2 emulates your usage.

func test1(x: consuming String) {
    _ = consume x // error: missing reinitialization of inout parameter 'x' after consume while accessing memory
    x = "abc"
    let fn: () -> Void = { print(x) }
    fn()
}

func test2(x: consuming String) {
    let fn: () -> Void = { _ = consume x } // error: missing reinitialization of inout parameter 'x' after consume while accessing memory
    fn()
}

PS: I think "inout parameter" in these error messages are incorrect.
PS2: As non-escaping closure doesn't have this issue, it appears that a variable referenced in defer code is considered captured by an escaping closure (that's my intuition too).