-
Notifications
You must be signed in to change notification settings - Fork 2.5k
[Proposal] ~Sendable Conformance for Suppressing Sendable Inference #3030
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
xedin
wants to merge
5
commits into
swiftlang:main
Choose a base branch
from
xedin:tilde-sendable
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
570bff1
[Proposal] ~Sendable Conformance for Suppressing Sendable Inference
xedin 2ddfb4c
~Sendable: Update the proposal to state that conditional conformances…
xedin 291a66b
~Sendable: Explain how suppression interacts with inherited and infer…
xedin 759e033
~Sendable: Add 'Future Directions' section
xedin cd10dee
~Sendable: Clarify why conditional extensions are allowed when where …
xedin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| # `~Sendable` Conformance for Suppressing Sendable Inference | ||
|
|
||
| * **Proposal**: [SE-NNNN](NNNN-tilde-sendable.md) | ||
| * **Authors**: [Pavel Yaskevich](https://github.com/xedin) | ||
| * **Review Manager**: TBD | ||
| * **Status**: **Pitch** | ||
| * **Implementation**: [implementation](https://github.com/swiftlang/swift/pull/84777), [Interaction with ObjC](https://github.com/swiftlang/swift/pull/85105) | ||
| * **Experimental Feature Flag**: `TildeSendable` | ||
| * **Review**: [pitch](https://forums.swift.org/t/pitch-sendable-conformance-for-suppressing-sendable-inference/83288) | ||
|
|
||
| ## Introduction | ||
|
|
||
| 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. | ||
|
|
||
|
|
||
| ## 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: | ||
|
|
||
|
|
||
| ```swift | ||
| class Base { | ||
| // ... | ||
| } | ||
| ``` | ||
|
|
||
|
|
||
|
|
||
| ```swift | ||
| @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`: | ||
|
|
||
|
|
||
| ```swift | ||
| 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`: | ||
|
|
||
| ```swift | ||
| // This type will never be inferred as Sendable before 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 unless they have an explicit `Sendable` requirement. | ||
|
|
||
| ## Detailed Design | ||
|
|
||
| The `~Sendable` conformance uses the tilde (`~`) prefix to indicate suppression similar to `~Copyable`, `~Escapable`, and `~BitwiseCopyable`: | ||
|
|
||
| ```swift | ||
| // 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 | ||
| } | ||
| ``` | ||
|
|
||
| Suppression must be declared on the struct, enum or class type declaration itself, not on an extension, because otherwise there is a risk of changing the meaning of the existing code: | ||
|
|
||
| ```swift | ||
| extension Test: ~Sendable {} // Error! | ||
| ``` | ||
|
|
||
| Attempting to suppress 'Sendable' conformance on generic parameters or protocol declarations would be rejected because they are always non-Sendable unless explicitly stated otherwise via `Sendable` requirement: | ||
|
|
||
| ```swift | ||
| protocol P: ~Sendable {} // Error! | ||
| struct Test<T: ~Sendable> {} // Error! | ||
| extension Array where Element: ~Sendable {} // Error! | ||
| ``` | ||
|
|
||
| Just like with unavailable `Sendable` extensions, types with `~Sendable` conformances cannot satisfy `Sendable` requirements: | ||
|
|
||
| ```swift | ||
| 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: | ||
|
|
||
| ```swift | ||
| 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: | ||
|
|
||
| ```swift | ||
| func test<T: ~Sendable>(_: T) {} // error: conformance to 'Sendable' can only be suppressed on structs, classes, and enums | ||
| ``` | ||
|
|
||
|
|
||
| Attempting to unconditionally conform to both `Sendable` and `~Sendable` results in a compile-time error: | ||
|
|
||
| ```swift | ||
| // Actors are always `Sendable`. | ||
| actor A: ~Sendable { // error: cannot both conform to and suppress conformance to 'Sendable' | ||
| } | ||
|
|
||
| struct Container<T>: ~Sendable { | ||
| let value: T | ||
| } | ||
|
|
||
| extension Container: Sendable {} // error: cannot both conform to and suppress conformance to 'Sendable' | ||
| ``` | ||
|
|
||
| This rule also applies to explicit and derived `Sendable` conformances inherited from superclasses and protocols: | ||
|
|
||
| ```swift | ||
| protocol IsolatedProtocol: Sendable { | ||
| } | ||
|
|
||
| struct Test: IsolatedProtocol, ~Sendable { // error: cannot both conform to and suppress conformance to 'Sendable' | ||
| } | ||
|
|
||
| @MainActor | ||
| class IsolatedBase { // global actor isolated types are `Sendable`. | ||
| } | ||
|
|
||
| class Refined: IsolatedBase, ~Sendable { // error: cannot both conform to and suppress conformance to 'Sendable' | ||
| } | ||
| ``` | ||
|
|
||
| Conditional conformances to `Sendable` protocol are still allowed: | ||
|
|
||
| ```swift | ||
| extension Container: Sendable where T: Sendable {} // Ok! | ||
xwu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ``` | ||
|
|
||
| Due to `Sendable` inference on types, it is still helpful to allow conditional `Sendable` conformances on `~Sendable` types when an API author would like to express that the type is only `Sendable` conditionally when `Sendable` conformance could otherwise be inferred e.g. by checking type's storage or isolation. `~Sendable` is not required in other cases even for auditing purposes (with `ExplicitSendable`, please see below) because the type would be non-Sendable unless explicitly stated otherwise on a primary declaration or in a conditional `Sendable` conformance extension. | ||
|
|
||
| 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 | ||
|
|
||
| ```swift | ||
| @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. | ||
|
|
||
xwu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ## Future Directions | ||
|
|
||
| This proposal is focusing excusively on `Sendable` protocol but other implicitly inferred protocol conformances - `Equatable`, `Hashable`, `RawRepresentable` - could also be suppressed using the `~` spelling, and would likewise benefit from being suppressible (for example, when the author of an enum wants to rely on the synthesized implementation of `==` that comes from `Equatable` instead of `RawRepresentable`). Each case like this has their nuances and might require a dedicated proposal. | ||
|
|
||
| ## Acknowledgements | ||
|
|
||
| Thank you to [Holly Borla](https://github.com/hborla) for the discussion and editorial help. | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see you've included this case already as well as GAITs below, based on pitch feedback. It would be good to assure ourselves that the text is now complete in terms of covering how the feature interacts with all of the inference rules applicable to
Sendableand not found onBitwiseCopyable(or other suppressible protocols).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you propose we do that?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know of a foolproof way, but as a starting point I'd imagine one could go through each of the inference rules for
Sendablein SE-0302 and SE-0418 [edit: as references to read and consider, not to repeat in this text], then following through to other inference rules incorporated by reference (for example: given actors implySendable, then look to actor inference rules).Looking at SE-0302, for example, it occurs to me that for completeness one might want to state that tuples cannot have their inferred
Sendableconformance disabled. And since this proposal prohibits using~Sendablein scenarios such as generic constraints, one thing we might want to be certain of is that the inference rules for those constraints don't inferSendable(e.g., for key paths) in a way that a user might want to disable and cannot.Even if it can't be 100%, that some effort has been made along these lines would no doubt help to round out the completeness of the proposal text by pointing out scenarios where some subtlety is underspecified or just worth clarifying.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. I added a paragraph to that matter in cd10dee.