[Pitch] Revise SE-0430 to adopt `sending` on `UnsafeContinuation`

SE-0430, as accepted, intentionally leaves UnsafeContinuation open as a hole for sendability checking for the return value. On second thought, I think we should revise the proposal to close that hole and require the return value to be sending. Programmers who need to bypass sendability checking can use the general nonisolated(unsafe) technique.

From the PR:

An earlier version of this proposal excluded UnsafeContinuation.resume(returning:) from the list of standard library APIs that adopt sending. This meant that UnsafeContinuation didn't require either the return type to be Sendable or the return value to be sending. Since UnsafeContinuation is unconditionally Sendable, this effectively made it a major hole in sendability checking.

This was an intentional choice. The reasoning was that UnsafeContinuation was already an explicitly unsafe type, and so it's not illogical for it to also work as an unsafe opt-out from sendability checks. There are some uses of continuations that do need an opt-out like this. For example, it is not uncommon for a continuation to be resumed from a context that's isolated to the same actor as the context that called withUnsafeContinuation. In this situation, the data flow through the continuation is essentially internal to the actor. This means there's no need for any sendability restrictions; both sending and Sendable would be over-conservative.

However, the nature of the unsafety introduced by this exclusion is very different from the normal unsafety of an UnsafeContinuation. Continuations must be resumed exactly once; that's the condition that CheckedContinuation checks. If a programmer can prove that they will do that to their own satisfaction, they should be able to use UnsafeContinuation instead of CheckedContinuation in full confidence. Making UnsafeContinuation also a potential source of concurrency-safety holes is likely to be surprising to programmers.

Conversely, if a programmer needs to opt out of sendability checks but is not confident about how many times their continuation will be resumed --- for example, if it's resumed from an arbitrary callback --- forcing them to adopt UnsafeContinuation in order to achieve their goal is actively undesirable.

Not requiring sending in UnsafeContinuation also makes the high-level interfaces of UnsafeContinuation and CheckedContinuation inconsistent. This means that programmers cannot always easily move from an unsafe to a checked continuation. That is a common need, for example when fixing a bug and trying to prove that the unsafe continuation is not implicated.

Swift has generally resisted adding new dimensions of unsafety to unsafe types this way. For example, UnsafePointer was originally specified as unconditionally Sendable in SE-0302, but that conformance was removed in SE-0331, and pointers are now unconditionally non-Sendable. The logic in both of those proposals closely parallels this one: at first, UnsafePointer was seen as an unsafe type that should not be burdened with partial safety checks, and then the community recognized that this was actually adding a new dimension of unsafety to how the type interacted with concurrency.

Finally, there is already a general unsafe opt-out from sendability checking: nonisolated(unsafe). It is better for Swift to encourage consistent use of a single unsafe opt-out than to build ad hoc opt-outs into many different APIs, because it is much easier to find, recognize, and audit uses of the former.

For these reasons, UnsafeContinuation.resume(returning:) now requires its argument to be sending, and the result of withUnsafeContinuation is correspondingly now marked as sending.

28 Likes