[Pitch] `~Sendable` Conformance for Suppressing Sendable Inference

Introduction

This pitch introduces ~Sendable conformance syntax to explicitly suppress a conformance to Sendable, which would prevent automatic Sendable inference on types, and provide an alternative way to mark types as non-Sendable without inheritance impact.

Motivation

When encountering a public type that doesn't explicitly conform to Sendable, it's difficult to determine the intent. It can be unclear whether the type should have an explicit Sendable conformance that hasn't been added yet, or whether it's deliberately non-Sendable. Making this determination requires understanding how the type's storage is structured and whether access to shared state is protected by a synchronization mechanism - implementation details which may not be accessible from outside the library.

There are also situations when a class is not Sendable but some of its subclasses are. There is currently a way to expression that a type does not conform to a Sendable protocol:

class Base {
   // ...
}
@available(*, unavailable)
extension Base: Sendable {
}

Like all other conformances, an unavailable conformance to Sendable is inherited by subclasses. An unavailable conformance means that the type never conforms to Sendable, including all subclasses. Attempting to declare a thread-safe subclass ThreadSafe:
Attempting to declare a thread-safe subclass ThreadSafe:

final class ThreadSafe: Base, @unchecked Sendable {
   // ...
}

is not possible and results in the following compiler warning:

warning: conformance of 'ThreadSafe' to protocol 'Sendable' is already unavailable

because unavailable conformance to Sendable is inherited by the subclasses.

This third state of a class not having a conformance to Sendable because subclasses may or may not conform to Sendable is not explicitly expressible in the language. Having an explicit spelling is important for library authors doing a comprehensive Sendable audit of their public API surface, and for communicating to clients that the lack of Sendable conformance is deliberate, while preserving the ability to add @unchecked Sendable conformances in subclasses.

Proposed Solution

Introduce ~Sendable conformance syntax that explicitly suppresses Sendable:

// This type will never be inferred as Sendable because though it could be inferred as such.
struct MyType: ~Sendable {
    let value: Int
}

This syntax is only applicable to types because other declarations like generic parameters are already effectively ~Sendable by default until they have an explicit Sendable requirement.

Detailed Design

The ~Sendable conformance uses the tilde (~) prefix to indicate suppression similar to ~Copyable, ~Escapable, and ~BitwiseCopyable:

// Suppress Sendable inference
struct NotSendableType: ~Sendable {
    let data: String
}

// Can be combined with other conformances
struct MyType: Equatable, ~Sendable {
    let id: UUID
}

// Works with classes
class MyClass: ~Sendable {
    private let data = 0
}

Just like with unavailable extensions, types with ~Sendable conformances cannot satisfy Sendable requirements:

func processData<T: Sendable>(_ data: T) { }

struct NotSendable: ~Sendable {
    let value: Int
}

processData(NotSendable(value: 42)) // error: type 'NotSendable' does not conform to the 'Sendable' protocol

But, unlike unavailable extensions, ~Sendable conformances do not affect subclasses:

class A: ~Sendable {
}

final class B: A, @unchecked Sendable {
}

func takesSendable<T: Sendable>(_: T) {
}

takesSendable(B()) // Ok!

Attempting to use ~Sendable as a generic requirement results in a compile-time error:

func test<T: ~Sendable>(_: T) {} // error: conformance to 'Sendable' can only be suppressed on structs, classes, and enums

Attempting to explicitly conform (both conditionally and unconditionally) to both Sendable and ~Sendable results in a compile-time error:

struct Container<T>: ~Sendable {
    let value: T
}

extension Container: Sendable {} // error: cannot both conform to and suppress conformance to 'Sendable'
extension Container: Sendable where T: Sendable {} // error: cannot both conform to and suppress conformance to 'Sendable'

The Swift compiler provides a way to audit Sendability of public types. The current way to do this is by enabling the -require-explicit-sendable flag to produce a warning for every public type without explicit Sendable conformance (or an unavailable extension). This flag now supports ~Sendable and has been turned into a diagnostic group that is disabled by default - ExplicitSendable, and can be enabled by -Wwarning ExplicitSendable.

Source Compatibility

This proposal is purely additive and maintains full source compatibility with existing code:

  • Existing code continues to work unchanged
  • No existing Sendable inference behavior is modified
  • Only adds new opt-in functionality

Effect on ABI Stability

~Sendable conformance is a compile-time feature and has no ABI impact:

  • No runtime representation
  • No effect on existing compiled code

Effect on API Resilience

The ~Sendable annotation affects API contracts:

  • Public API: Adding ~Sendable to a public type does not impact source compatibility because Sendable inference does not apply to public types. Changing a Sendable conformance to ~Sendable is a source breaking change.

Alternatives Considered

@nonSendable Attribute

@nonSendable
struct MyType {
    let value: Int
}

Protocol conformance is more ergonomic considering the inverse case, and it follows the existing convention of conformance suppression to other marker protocols.

Acknowledgements

Thank you to Holly Borla for the discussion and editorial help.

15 Likes

Since a generic requirement T, with no constraints (or other inference rules), would not assume Sendable, the declarations func test<T: ~Sendable>(_: T) { } and func test<T>(_: T) { } mean the same thing--correct?

I can see a case for a warning to be made for redundancy, but since the "not-known-to-be-sendable"-ness of T is the case anyway, is there a reason why the compiler absolutely must reject writing it out explicitly?

1 Like

This is just to make it consistent with ~BitwiseCopyable but I can change it to warning if that’s preferable.

Super excited to see this feature. We’ve wanted to be able to express this concept in Foundation because of the subclass case that you mention (there are a number of classes in Foundation that are not concretely non-Sendable but have both Sendable and non-Sendable subclasses and we’ve been unable to annotate the parent classes for completeness thus far - see these occurrences).

One minor clarifying question: is writing ~Sendable in an extension supported? For example:

extension Foo: ~Sendable {}

I don’t see a strong use case for this as opposed to annotating the type itself (aside from separating conformances into extensions), but it was unclear to me whether this would be akin to the generic clause case (in which ~Sendable would be rejected) or if it would be accepted assuming no other Sendable conformance exists for Foo.

It’s not, similarly to ~BitwiseCopyable as well, conformance can only be added directly to inheritance clause of a type.

1 Like

Could we also consider deprecating and phasing out behind an upcoming feature the implicit inference of a Sendable conformance on non-public types?

I think it’s out of scope of this proposal and should be discussed separately since that would at a very least have source compatibility impact.

+1, really excited about this feature, especially the subclassing behavior! It’s going to be really helpful for library authors when auditing for Sendability :slight_smile:

1 Like

Is ~Sendable allowed on a protocol declaration? Extension of protocol can't have inheritance clause so we don't have any case of

protocol P {}
@available(*, unavailable)
extension P: Sendable {}

So, what does protocol P: ~Sendable do, if it's allowed syntactically?

1 Like

No, it’s not allowed on protocols, only class, struct and enum declarations.

2 Likes

minor editorial side question – what is this comment trying to communicate?

2 Likes

Thanks! It should say “before” instead of “because” :slight_smile:

1 Like