Accessing sendable parts of non-sendable tuples within deinit

I would like to write something like the following:

final class NonSendable {}

@MainActor
final class Foo {

  var stream: (source: NonSendable, consumer: Task<Void, any Error>)?

  deinit {
    stream?.consumer.cancel()
  }
}

Unfortunately, it fails to compile with -swift-version 6:

<source>:8:11: error: cannot access property 'stream' with a non-sendable type '(source: NonSendable, consumer: Task<Void, any Error>)?' from nonisolated deinit
 1 | final class NonSendable {}
   |             `- note: class 'NonSendable' does not conform to the 'Sendable' protocol
 2 | 
 3 | @MainActor
   :
 6 | 
 7 |   deinit {
 8 |     stream?.consumer.cancel()
   |           `- error: cannot access property 'stream' with a non-sendable type '(source: NonSendable, consumer: Task<Void, any Error>)?' from nonisolated deinit
 9 |   }
10 | }

Now, I understand that deinit may be invoked from any concurrency domain, so I cannot be allowed to call any methods on stream?.source (other than deinit, but we can't call that explicitly).

But I don't think the access to the stream property itself should be considered unsafe - after all, within a deinit accesses to the property cannot possibly race because there are no other living references to this object. And there is part of the tuple which I can use from any concurrency context.

Is this perhaps an edge-case that hasn't been accounted for/is difficult to implement, and could it potentially be allowed in future?

2 Likes

Actually, I was thinking a bit more about this:

within a deinit accesses to the property cannot possibly race because there are no other living references to this object

But what about weak references?

Is it possible that an access through a weak reference (on the main actor in this case) could race with the deinit running in another concurrency domain?

And if so, what does the compiler do? At the end of my object's deinit, the compiler is also going to have to access this variable, destructure this optional, destructure the tuple (in the same way that I want to, to access stream?.consumer), and deinitialise the constituent parts. Couldn't those also race with a weak reference?

Or do we know that all weak references are nulled out before deinit runs?

EDIT: Yes. There's a lot of stuff related to deinit and ensuring weak references do not race with it. Cool.

Okay, sorry for the aside - so we can assume nothing is racing with us inside a deinit.