Skip to content

Commit 74bb8ce

Browse files
committed
Disconnected type proposal
1 parent 00298ba commit 74bb8ce

1 file changed

Lines changed: 269 additions & 0 deletions

File tree

proposals/NNNN-disconnected.md

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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

Comments
 (0)