|
| 1 | +# `~Sendable` Conformance for Suppressing Sendable Inference |
| 2 | + |
| 3 | +* **Proposal**: [SE-NNNN](NNNN-tilde-sendable.md) |
| 4 | +* **Authors**: [Pavel Yaskevich](https://github.com/xedin) |
| 5 | +* **Review Manager**: TBD |
| 6 | +* **Status**: **Pitch** |
| 7 | +* **Implementation**: [implementation](https://github.com/swiftlang/swift/pull/84777), [Interaction with ObjC](https://github.com/swiftlang/swift/pull/85105) |
| 8 | +* **Experimental Feature Flag**: `TildeSendable` |
| 9 | +* **Review**: [pitch](https://forums.swift.org/t/pitch-sendable-conformance-for-suppressing-sendable-inference/83288) |
| 10 | + |
| 11 | +## Introduction |
| 12 | + |
| 13 | +This proposal 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. |
| 14 | + |
| 15 | + |
| 16 | +## Motivation |
| 17 | + |
| 18 | +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. |
| 19 | + |
| 20 | +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: |
| 21 | + |
| 22 | + |
| 23 | +```swift |
| 24 | +class Base { |
| 25 | + // ... |
| 26 | +} |
| 27 | +``` |
| 28 | + |
| 29 | + |
| 30 | + |
| 31 | +```swift |
| 32 | +@available(*, unavailable) |
| 33 | +extension Base: Sendable { |
| 34 | +} |
| 35 | +``` |
| 36 | + |
| 37 | + |
| 38 | +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`: |
| 39 | +Attempting to declare a thread-safe subclass `ThreadSafe`: |
| 40 | + |
| 41 | + |
| 42 | +```swift |
| 43 | +final class ThreadSafe: Base, @unchecked Sendable { |
| 44 | + // ... |
| 45 | +} |
| 46 | +``` |
| 47 | + |
| 48 | + |
| 49 | +is not possible and results in the following compiler warning: |
| 50 | + |
| 51 | + |
| 52 | +``` |
| 53 | +warning: conformance of 'ThreadSafe' to protocol 'Sendable' is already unavailable |
| 54 | +``` |
| 55 | + |
| 56 | + |
| 57 | +because unavailable conformance to `Sendable` is inherited by the subclasses. |
| 58 | + |
| 59 | +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. |
| 60 | + |
| 61 | + |
| 62 | +## Proposed Solution |
| 63 | + |
| 64 | +Introduce `~Sendable` conformance syntax that explicitly suppresses `Sendable`: |
| 65 | + |
| 66 | +```swift |
| 67 | +// This type will never be inferred as Sendable before though it could be inferred as such. |
| 68 | +struct MyType: ~Sendable { |
| 69 | + let value: Int |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +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. |
| 74 | + |
| 75 | + |
| 76 | +## Detailed Design |
| 77 | + |
| 78 | +The `~Sendable` conformance uses the tilde (`~`) prefix to indicate suppression similar to `~Copyable`, `~Escapable`, and `~BitwiseCopyable`: |
| 79 | + |
| 80 | +```swift |
| 81 | +// Suppress Sendable inference |
| 82 | +struct NotSendableType: ~Sendable { |
| 83 | + let data: String |
| 84 | +} |
| 85 | + |
| 86 | +// Can be combined with other conformances |
| 87 | +struct MyType: Equatable, ~Sendable { |
| 88 | + let id: UUID |
| 89 | +} |
| 90 | + |
| 91 | +// Works with classes |
| 92 | +class MyClass: ~Sendable { |
| 93 | + private let data = 0 |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +Just like with unavailable extensions, types with `~Sendable` conformances cannot satisfy `Sendable` requirements: |
| 98 | + |
| 99 | +```swift |
| 100 | +func processData<T: Sendable>(_ data: T) { } |
| 101 | + |
| 102 | +struct NotSendable: ~Sendable { |
| 103 | + let value: Int |
| 104 | +} |
| 105 | + |
| 106 | +processData(NotSendable(value: 42)) // error: type 'NotSendable' does not conform to the 'Sendable' protocol |
| 107 | +``` |
| 108 | + |
| 109 | + |
| 110 | +But, unlike unavailable extensions, `~Sendable` conformances do not affect subclasses: |
| 111 | + |
| 112 | +```swift |
| 113 | +class A: ~Sendable { |
| 114 | +} |
| 115 | + |
| 116 | +final class B: A, @unchecked Sendable { |
| 117 | +} |
| 118 | + |
| 119 | +func takesSendable<T: Sendable>(_: T) { |
| 120 | +} |
| 121 | + |
| 122 | +takesSendable(B()) // Ok! |
| 123 | +``` |
| 124 | + |
| 125 | + |
| 126 | +Attempting to use `~Sendable` as a generic requirement results in a compile-time error: |
| 127 | + |
| 128 | +```swift |
| 129 | +func test<T: ~Sendable>(_: T) {} // error: conformance to 'Sendable' can only be suppressed on structs, classes, and enums |
| 130 | +``` |
| 131 | + |
| 132 | + |
| 133 | +Attempting to explicitly conform (both conditionally and unconditionally) to both `Sendable` and `~Sendable` results in a compile-time error: |
| 134 | + |
| 135 | +```swift |
| 136 | +struct Container<T>: ~Sendable { |
| 137 | + let value: T |
| 138 | +} |
| 139 | + |
| 140 | +extension Container: Sendable {} // error: cannot both conform to and suppress conformance to 'Sendable' |
| 141 | +extension Container: Sendable where T: Sendable {} // error: cannot both conform to and suppress conformance to 'Sendable' |
| 142 | +``` |
| 143 | + |
| 144 | +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`. |
| 145 | + |
| 146 | +## Source Compatibility |
| 147 | + |
| 148 | +This proposal is purely additive and maintains full source compatibility with existing code: |
| 149 | + |
| 150 | +* Existing code continues to work unchanged |
| 151 | +* No existing `Sendable` inference behavior is modified |
| 152 | +* Only adds new opt-in functionality |
| 153 | + |
| 154 | +## Effect on ABI Stability |
| 155 | + |
| 156 | +`~Sendable` conformance is a compile-time feature and has no ABI impact: |
| 157 | + |
| 158 | +* No runtime representation |
| 159 | +* No effect on existing compiled code |
| 160 | + |
| 161 | +## Effect on API Resilience |
| 162 | + |
| 163 | +The `~Sendable` annotation affects API contracts: |
| 164 | + |
| 165 | +* **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. |
| 166 | + |
| 167 | +## Alternatives Considered |
| 168 | + |
| 169 | +### `@nonSendable` Attribute |
| 170 | + |
| 171 | +```swift |
| 172 | +@nonSendable |
| 173 | +struct MyType { |
| 174 | + let value: Int |
| 175 | +} |
| 176 | +``` |
| 177 | + |
| 178 | +Protocol conformance is more ergonomic considering the inverse case, and it follows the existing convention of conformance suppression to other marker protocols. |
| 179 | + |
| 180 | + |
| 181 | +## Acknowledgements |
| 182 | + |
| 183 | +Thank you to [Holly Borla](https://github.com/hborla) for the discussion and editorial help. |
0 commit comments