|
| 1 | +# Disconnected |
| 2 | + |
| 3 | +* Proposal: [SE-NNNN](NNNN-disconnected.md) |
| 4 | +* Authors: [Franz Busch](https://github.com/FranzBusch) |
| 5 | +* Review Manager: TBD |
| 6 | +* Status: **Awaiting implementation** |
| 7 | +* Implementation: [swiftlang/swift#NNNNN](https://github.com/swiftlang/swift/pull/NNNNN) |
| 8 | +* Review: ([pitch](https://forums.swift.org/...)) |
| 9 | + |
| 10 | +## Introduction |
| 11 | + |
| 12 | +[SE-0414](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0414-region-based-isolation.md) |
| 13 | +introduced region-based isolation which leverages control flow sensitive |
| 14 | +diagnostics to determine whether non-`Sendable` values are safe to send across |
| 15 | +isolation boundaries. |
| 16 | +[SE-0430](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0430-transferring-parameters-and-results.md) |
| 17 | +introduced the `sending` parameter and result annotation to explicitly mark |
| 18 | +values that must be in a disconnected region at function boundaries. |
| 19 | + |
| 20 | +This proposal introduces a `Disconnected` type that preserves the disconnected |
| 21 | +property of a value through storage in data structures, allowing generic types |
| 22 | +to safely transfer non-`Sendable` values across isolation regions without |
| 23 | +requiring those types to reason about the `sending` effect. |
| 24 | + |
| 25 | +## Motivation |
| 26 | + |
| 27 | +Region-based isolation enables transferring non-`Sendable` values across |
| 28 | +isolation boundaries when the value is in a disconnected region. The `sending` |
| 29 | +parameter and result annotation from SE-0430 allows functions to explicitly |
| 30 | +require disconnected values at function boundaries. However, `sending` cannot |
| 31 | +be preserved through stored properties, collection types, or generic containers. |
| 32 | + |
| 33 | +Consider a queue implementation that stores elements to be processed across |
| 34 | +isolation boundaries. As an example, let's look at a hypothetical `UniqueDeque`: |
| 35 | + |
| 36 | +```swift |
| 37 | +struct UniqueDeque<Element: ~Copyable>: ~Copyable { |
| 38 | + func append(_ element: consuming Element) { ... } |
| 39 | + func popFirst() -> Element? { ... } |
| 40 | +} |
| 41 | +``` |
| 42 | + |
| 43 | +One use-case might want to use the `UniqueDeque` to append non-`Sendable` |
| 44 | +disconnected values and when popping an element send it to a different isolation |
| 45 | +region. |
| 46 | + |
| 47 | +```swift |
| 48 | +var deque = UniqueDeque<NonSendable>() |
| 49 | +deque.append(NonSendable()) |
| 50 | + |
| 51 | +guard let element = deque.popFirst() else { return } |
| 52 | + |
| 53 | +Task { |
| 54 | + print(element) // Error: Element is assumed to be in the same isolation region as uniqueDeque |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +To make this work we would need to consume the element in `append` and |
| 59 | +return it from `popFirst` as `sending`; however, this would significantly limit |
| 60 | +this type for other important use-cases where users want to store non-`Sendable` |
| 61 | +but **not disconnected** elements. |
| 62 | + |
| 63 | +The fundamental limitation is that `sending` is a property of function |
| 64 | +boundaries, not types. Generic types like `UniqueDeque` cannot conditionally |
| 65 | +apply region isolation based on whether their element type should maintain |
| 66 | +disconnected regions. Making `append` and `popFirst` use `sending` would |
| 67 | +prevent legitimate use cases where elements should remain in the same region. |
| 68 | + |
| 69 | +## Proposed solution |
| 70 | + |
| 71 | +This proposal introduces a new `Disconnected` type that allows us to model |
| 72 | +a disconnected value. |
| 73 | + |
| 74 | +```swift |
| 75 | +var deque = UniqueDeque<Disconnected<NonSendable>>() |
| 76 | +deque.append(Disconnected(NonSendable())) |
| 77 | + |
| 78 | +guard let disconnected = deque.popFirst() else { return } |
| 79 | + |
| 80 | +Task { |
| 81 | + let element = disconnected.take() |
| 82 | + print(element) |
| 83 | +} |
| 84 | +``` |
| 85 | + |
| 86 | +The `Disconnected` type wraps a value, ensuring it remains in a disconnected |
| 87 | +region. The `take()` method consumes the `Disconnected` wrapper and returns |
| 88 | +the value as `sending`, allowing it to cross isolation boundaries. |
| 89 | + |
| 90 | +## Detailed design |
| 91 | + |
| 92 | +The `Disconnected` type is a simple wrapper that enforces region isolation |
| 93 | +through the type system: |
| 94 | + |
| 95 | +```swift |
| 96 | +/// A type that wraps a value in a disconnected isolation region. |
| 97 | +/// |
| 98 | +/// Values of type `Disconnected<T>` are guaranteed to be in a disconnected |
| 99 | +/// region, meaning they have no references to or from other isolation regions. |
| 100 | +/// This allows them to be safely transferred across isolation boundaries and |
| 101 | +/// stored in data structures that preserve the disconnected property. |
| 102 | +@frozen |
| 103 | +public struct Disconnected<Value: ~Copyable>: ~Copyable, Sendable { |
| 104 | + /// Initializes a new disconnected value by consuming the passed value. |
| 105 | + /// |
| 106 | + /// The value must be in a disconnected region. This is enforced by |
| 107 | + /// requiring the parameter to be `sending`. |
| 108 | + /// |
| 109 | + /// - Parameter value: The value to wrap in a disconnected region. |
| 110 | + public init(_ value: consuming sending Value) |
| 111 | + |
| 112 | + /// Provides borrowing access to the wrapped value without consuming the |
| 113 | + /// wrapper. |
| 114 | + /// |
| 115 | + /// Because this is a `borrow` accessor, the wrapped value cannot be |
| 116 | + /// mutated or replaced through it, preserving the disconnected region |
| 117 | + /// property of the wrapper. |
| 118 | + public var value: Value { borrow } |
| 119 | + |
| 120 | + /// Consumes the disconnected wrapper and returns the underlying value. |
| 121 | + /// |
| 122 | + /// The returned value is `sending`, indicating it is in a disconnected |
| 123 | + /// region and can be transferred across isolation boundaries. |
| 124 | + /// |
| 125 | + /// - Returns: The wrapped value as a `sending` result. |
| 126 | + public consuming func take() -> sending Value |
| 127 | + |
| 128 | + /// Swaps the current disconnected value with a new one. |
| 129 | + /// |
| 130 | + /// The returned value is `sending`, indicating it is in a disconnected |
| 131 | + /// region and can be transferred across isolation boundaries. |
| 132 | + /// |
| 133 | + /// - Parameter newValue: The new value to wrap in a disconnected region. |
| 134 | + mutating func swap(newValue: consuming sending Value) -> sending Value |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +The `Disconnected` type conforms to `Sendable` because it guarantees its wrapped |
| 139 | +value is in a disconnected region. Since disconnected regions can be safely |
| 140 | +transferred across isolation boundaries, `Disconnected<T>` is safe to share |
| 141 | +regardless of whether `T` conforms to `Sendable`. The `value` borrow accessor |
| 142 | +is sound because it cannot mutate or replace the wrapped value, so the |
| 143 | +disconnection invariant is preserved for the duration of the borrow. |
| 144 | +Furthermore, all mutating methods on `Disconnected` are either `consuming` or |
| 145 | +`mutating` which means that the compiler will enforce static and dynamic |
| 146 | +exclusivity checking prohibiting overlapping and concurrent access. |
| 147 | + |
| 148 | +This shape also composes naturally with the borrowing accessors on generic |
| 149 | +containers introduced by |
| 150 | +[SE-0519](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0519-ref-mutableref-types.md). |
| 151 | +A container holding `Disconnected<Value>` elements can expose a `Ref<Element>` |
| 152 | +projection without any knowledge of `Disconnected`, and callers can drill |
| 153 | +through to the wrapped value via the `value` accessor. |
| 154 | + |
| 155 | +## Source compatibility |
| 156 | + |
| 157 | +This proposal adds a new type to the standard library. No existing code is |
| 158 | +affected. |
| 159 | + |
| 160 | +## ABI compatibility |
| 161 | + |
| 162 | +This proposal adds a new `@frozen` type to the standard library. The layout of |
| 163 | +`Disconnected` is ABI stable. No existing ABI is affected. |
| 164 | + |
| 165 | +## Implications on adoption |
| 166 | + |
| 167 | +The additions described in this proposal require a new version of the Swift |
| 168 | +standard library and runtime. |
| 169 | + |
| 170 | +## Future directions |
| 171 | + |
| 172 | +### Support for `~Escapable` values |
| 173 | + |
| 174 | +The current design restricts `Disconnected` to escapable types. The disconnected |
| 175 | +region property is conceptually independent of lifetime dependencies, so it is |
| 176 | +tempting to relax the `Value` constraint and make `Disconnected` conditionally |
| 177 | +`Escapable`: |
| 178 | + |
| 179 | +```swift |
| 180 | +struct Disconnected<Value: ~Copyable & ~Escapable>: ~Copyable, ~Escapable, Sendable { ... } |
| 181 | +extension Disconnected: Escapable where Value: Escapable {} |
| 182 | +``` |
| 183 | + |
| 184 | +However, this generalization is not useful in practice. Nonescapable types as |
| 185 | +introduced by |
| 186 | +[SE-0446](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0446-non-escapable.md) |
| 187 | +are non-owning views with a lifetime dependency on some source storage (e.g. |
| 188 | +`MutableSpan<Element>` borrows from an `Array<Element>`). This creates two |
| 189 | +problems: |
| 190 | + |
| 191 | +1. **No `sending` form exists at the source.** View types are produced by |
| 192 | + borrowing accessors that return a value with a lifetime dependency on `self`. |
| 193 | + There is no `sending` accessor to consume, so |
| 194 | + `Disconnected(array.mutableSpan)` cannot even be constructed. |
| 195 | +2. **The lifetime source does not travel with the wrapper.** Even if a `sending` |
| 196 | + view could be produced, the view still carries a reference into storage that |
| 197 | + lives elsewhere. Transferring `Disconnected<MutableSpan<Int>>` to another |
| 198 | + isolation region leaves the backing `Array` behind, violating the |
| 199 | + disconnected region property by construction. A generic wrapper has no way to |
| 200 | + know what the lifetime source is or to carry it along. |
| 201 | + |
| 202 | +The proposed `Disconnected` type should be used to transfer the actual storage |
| 203 | +between isolation regions. |
| 204 | + |
| 205 | +## Alternatives considered |
| 206 | + |
| 207 | +### Alternative names |
| 208 | + |
| 209 | +Different names such as `Nonisolated` and `DisconnectedRegion` were considered; |
| 210 | +however, the name `Disconnected` felt the most fitting. Furthermore, the concept |
| 211 | +of a disconnected region was introduced in previous proposals. |
| 212 | + |
| 213 | +### Using `sending` annotations on generic parameters |
| 214 | + |
| 215 | +Rather than introducing a wrapper type, we could attempt to parameterize generic |
| 216 | +types over whether their elements are `sending`. This would require significant |
| 217 | +language changes to support conditional application of `sending` based on |
| 218 | +generic constraints, and would complicate generic type signatures. The wrapper |
| 219 | +type approach provides equivalent functionality with no language changes beyond |
| 220 | +the library addition. |
| 221 | + |
| 222 | +### Making `Disconnected` a protocol |
| 223 | + |
| 224 | +A `Disconnected` protocol could be applied to existing types. However, this |
| 225 | +would require proving that all values of conforming types are in disconnected |
| 226 | +regions, which cannot be enforced for mutable types. The wrapper type approach |
| 227 | +provides stronger guarantees by construction. |
| 228 | + |
| 229 | +### Exposing `Ref` and `MutableRef` projections |
| 230 | + |
| 231 | +Rather than (or in addition to) the `value` borrow accessor, `Disconnected` |
| 232 | +could expose dedicated projections producing the reference types from |
| 233 | +[SE-0519](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0519-ref-mutableref-types.md): |
| 234 | + |
| 235 | +```swift |
| 236 | +extension Disconnected where Value: ~Copyable { |
| 237 | + public var ref: Ref<Value> { borrow } |
| 238 | + public var mutableRef: MutableRef<Value> { mutate } // unsound, see below |
| 239 | +} |
| 240 | +``` |
| 241 | + |
| 242 | +A `ref: Ref<Value>` projection would be sound for the same reason the `value` |
| 243 | +borrow accessor is sound: `Ref.value` is itself a `borrow` accessor, and |
| 244 | +`Ref<Value>` is `Sendable` only when `Value` is `Sendable`, so a |
| 245 | +`Ref<NonSendable>` cannot be exfiltrated to another isolation region. However, |
| 246 | +it is redundant: callers who want a `Ref` can construct one explicitly from the |
| 247 | +`value` accessor, and generic containers built on SE-0519 will naturally produce |
| 248 | +`Ref<Disconnected<Value>>` without `Disconnected` needing to participate. Adding |
| 249 | +a dedicated `ref` property would duplicate the existing borrow accessor without |
| 250 | +enabling anything new. |
| 251 | + |
| 252 | +A `mutableRef: MutableRef<Value>` projection, by contrast, would be unsound. |
| 253 | +`Disconnected: Sendable` is unconditional, which means the type system trusts |
| 254 | +the wrapper to keep its contents in a disconnected region. The setter on |
| 255 | +`MutableRef.value` accepts any `Value` in the current region without a `sending` |
| 256 | +constraint, so it would allow code like: |
| 257 | + |
| 258 | +```swift |
| 259 | +var disconnected = Disconnected(NonSendable()) |
| 260 | +disconnected.mutableRef.value = nonDisconnectedValue // silently merges regions |
| 261 | +// disconnected.take() now hands out a "sending" value that isn't disconnected |
| 262 | +``` |
| 263 | + |
| 264 | +Mutating methods reached through `mutableRef.value` could capture references |
| 265 | +into other regions in the same way. The existing `swap` method covers the sound |
| 266 | +version of "replace the wrapped value with a new one" by requiring `sending` for |
| 267 | +the replacement, and is the only mutating projection that can preserve the |
| 268 | +disconnection invariant without language-level support for `sending`-constrained |
| 269 | +mutation. |
0 commit comments