# \[Pitch\] Safely sending non-\`Sendable\` values across isolation domains

**URL:** <https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566>\
**Category:** Pitches\
**Created:** [August 3, 2023, 11:08pm UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566 "2023-08-03T23:08:42Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![JTurcotti](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jturcotti/32/45226_2.png) [@JTurcotti](https://forums.swift.org/u/JTurcotti)\
**Post date:** [August 3, 2023, 11:08pm UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/1 "2023-08-03T23:08:42Z")

</div>

Hello Swift Community!

As strict concurrency checking comes closer to being a standard, error-producing part of the Swift language, the restriction that only `Sendable` values can be communicated between tasks, actors, and other isolation domains is becoming increasingly problematic. For many datatypes, sacrificing mutability to attain `Sendable` conformance is awkward to program around. A particularly unfortunate case arises for datatypes that _cannot_ safely be made `Sendable`; computation involving them is forced onto actors where it hurts performance. Here's an example:

```swift
struct LocationData : Sendable {}

class NearestNeighbors {
    class DataPoint {
        var nearestPoints: [DataPoint] = [] ...
    }
    
    var rootPoints: [DataPoint] = [] ...
}

func computeNearestNeighbors(data : LocationData) -> NearestNeighbors { ... }

@MainActor func addToDisplay(neighbors : NearestNeighbors) { ... }

func computeAndDisplayData(data : LocationData) async {
    let neighbors = computeNearestNeighbors(data: data)
    await addToDisplay(neighbors: neighbors)
}

```

`NearestNeighbors` is an example of a class that _cannot_ be made `Sendable`. Instances are constructed by `computeNearestNeighbors` from a set of datapoints with location information, creating a graph associating each with its nearest neighbors (trust me, all that expensive computation happens in the ellipses). Since this is a certainly cyclic data structure, there's no way to build it with immutable `let` bindings alone, so it's bound to be non-`Sendable`. But eventually, we want to display it to UI, using the `@MainActor` to do so. Since there's no way for it to cross isolation domains, this means the expensive `computeNearestNeighbors` call has to happen on the main actor as well. This is bad for performance.

The `SendNonSendable` proposal, [linked here](https://github.com/JTurcotti/swift-evolution/blob/main/proposals/nnnn-send-non-sendable.md), describes a pass that uses flow-sensitive checking, plus a cool idea called "regions", to make many patterns involving sending non-Sendable values between isolation domains safe, including the above code. To the more type-theory-inclined among you - these are [linear regions](https://link.springer.com/chapter/10.1007/11693024_2).

Here's the link to the proposal:

> <https://github.com/JTurcotti/swift-evolution/blob/main/proposals/nnnn-send-non-sendable.md>

And here's the link to the PLDI '22 paper that this work is based on in case you want even more detail than provided in the proposal. Keep in mind the system in this paper is much more complex, and includes features such as `iso` fields not being implemented by this proposal.

> **[fearless.pdf](https://www.cs.cornell.edu/andru/papers/gallifrey-types/fearless.pdf)**
>
> 315.18 KB

If you want to play with the current implementation yourself, it's available on the public Github version of the Swift compiler, just make sure you run with flags `-enable-experimental-feature SendNonSendable -strict-concurrency=complete`.

Let me know what you think 😁🧵

---

<div class="post-metadata">

**Author:** ![wadetregaskis](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/wadetregaskis/32/8494_2.png) [@wadetregaskis](https://forums.swift.org/u/wadetregaskis)\
**Post date:** [August 4, 2023, 12:51am UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/2 "2023-08-04T00:51:24Z")

</div>

> [@JTurcotti](#):
>
> It tracks all non-`Sendable` values within function scopes, allowing them to be sent between isolation domains but marking them as "transferred" when they are. Transferred values can henceforth not be accessed.

Can you elaborate on the difference between this and moveable types? You have a section at the end, "Relying on move-only (i.e. purely linear) types", which presumably would cover this, but it's empty.

---

<div class="post-metadata">

**Author:** ![JTurcotti](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jturcotti/32/45226_2.png) [@JTurcotti](https://forums.swift.org/u/JTurcotti)\
**Post date:** [August 4, 2023, 1:53am UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/3 "2023-08-04T01:53:13Z")

</div>

Both this system and move-only type have in common _linearity_: they allow values to be _consumed_ somehow - indicating they can't be used thereafter. This means that both move-only types and the region-based types presented in this proposal could theoretically be used to make sending non-sendable types safe; both offer a mechanism for consuming values when they're sent to another thread, preventing races from occurring against the original thread.

But that's where the similarities end. In move-only types, individual values are treated linearly. This means, for one thing, that it's impossible to make aliases of values - the act of creating the alias is either banned or consumes the original value. Trying to point a field of a struct to a value also is banned or consumes it. This is the "single ownership" semantics you get in languages like Rust with some linearity. This allows values to safely be sent to other threads without introducing data races because the reference to a value used to make the send is guaranteed to be the only one.

Single ownership is _not_ the way region-based types like in this proposal work. Instead of banning the creation of aliases or ancillary references to a value, we allow but track them by putting them "in the same region" as the aliased or referenced value. This means that, until you reach a point that performs a concurrent operation like a send, you wouldn't even notice the presence of a region-based type system; it's doing all of its tracking "in the background". When you get to a point where a send is performed a linear consumption does take place. But not of just the value being sent, of the _entire region_ containing that value. This is just as safe as the purely linear, single ownership semantics of move-only types (and Rust). You aren't guaranteed to be using the only existing reference to perform the send, but you know all of the other ones and can render them invalid statically.

You have to be careful with regions, a lot of things count as merging regions and a lot of things count as consuming regions (basically anything that allows it to "escape") - but it works! And you got a lot more expressivity than pure linearity in return.

tl;dr move-only types **prevent** aliasing from arising, region-based types allow aliasing to arise but **track** it when it does.

what this looks like in practice:

```swift
let x = NonSendableClass()
let y = x
Task { print (x) }
print(y)

```

This is a race between the calls to `print(x)` and `print(y)`. Move-only types would prevent the race by either disallowing the alias `y` from being created, or invalidating `x` when it is. Region-based types like in this proposal would prevent the race by noticing `x` and `y` are "in the same region", so giving one to a `Task` also consumes the other.

Hope that clarifies, but let me know if not!

---

<div class="post-metadata">

**Author:** ![wadetregaskis](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/wadetregaskis/32/8494_2.png) [@wadetregaskis](https://forums.swift.org/u/wadetregaskis)\
**Post date:** [August 4, 2023, 4:17am UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/4 "2023-08-04T04:17:48Z")

</div>

Thank you, that's very clear.

It sounds like this is superior to move-only types, at least from a concurrency perspective (I'm guessing there's still unique utility in move-only types for other purposes, maybe from an RAII angle?). As someone that respects Rust's strictness but prefers Swift's pragmatic compromises, this approach sounds very appealing.

Maybe it's even a baby step closer to broader use of flow analysis in Swift, which I'm a huge proponent of.

#### What's the catch? 😁

Is the "downside", such as there may be one, just that it's hard to do this (in the compiler), or are there other caveats?

…some of the topics in 'future directions' sound like they might be pain-points, particularly the function arg & actor members aspects.

#### Prior art

I'm quite unfamiliar with this area of compiler theory - it sounds like this is a quite recent idea, and that's why it's not seen in other languages yet? _Are_ there any other languages that have done this already?

#### Diagnostics

Thanks also for the diagnostics section with the details and examples. I feel like the impact on compiler errors & warnings is sometimes a bit overlooked in pitches.

---

<div class="post-metadata">

**Author:** ![FranzBusch](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/franzbusch/32/19447_2.png) [@FranzBusch](https://forums.swift.org/u/FranzBusch)\
**Post date:** [August 5, 2023, 7:33am UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/5 "2023-08-05T07:33:49Z")

</div>

In general, I am a big fan of tackling this problem since we have a few holes in the current Sendable checking and once we close this it will be almost infectious and require Sendable for a bunch of things that one really only wants to transfer. I haven’t read this pitch in detail yet but will do so next week.

One thing that I like and find particularly interesting is that we do this with flow analysis which IMO is nice but the pitch states that this is enough to know if something can be transferred. I am not sure if I agree with this statement. A type could store state in thread locals and be truly not Sendable but we Analyse it to be safe due to flow control. I like how Rust achieved this by having two marker traits with `Send` and `Sync`. That gives the developer more control and all of this becomes a bit less magical.

Secondly what I think this proposal still doesn’t solve is the actor case where you want to isolate a non Sendable value to the actor for thread safety. If that non Sendable value has an async method then we will have to send it across isolation domains. I think one of your examples hinted at this but have to give it a more detailed read. We should at least call out that this proposal is not fixing this .

Lastly, could we add an example around how this interacts with child tasks in task groups. A very common scenario here is iterators from async sequences. Most async sequences only support a single iterator so it is important that this iterator is created and never accessed from two threads. This means it is marked as explicitly non Sendable but what we really want is just to make sure it is only held by a single task at a time. What I would like to see is an example where we construct an iterator, consume an element and then transfer it to a child task for further consumption. At best even getting it out of that child task again at the end by returning it.

Edit: How is this going to work across module? I am seeing a pattern with a lot of `with` style functions that are just glorified task groups that run a closure with access to some managed resource. Would we be able to infer that certain parameters are just transferred? I can imagine a specific transfer/send annotation to be helpful here

I will give this a more through read next week!

Franz

---

<div class="post-metadata">

**Author:** ![JTurcotti](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jturcotti/32/45226_2.png) [@JTurcotti](https://forums.swift.org/u/JTurcotti)\
**Post date:** [August 5, 2023, 11:45pm UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/6 "2023-08-05T23:45:02Z")

</div>

Here are three potential "catches" (roughly in decreasing order of importance)

1. **Compile-time cost** : The current implementation relies on performing a fixpoint iteration to find the region partition that solves the dataflow equations of the function being analyzed. In practice, it appears to be very low, with most function with `n` basic blocks converging in at most `2n` iterations, but it's trivial to construct pathological examples with 3 basic blocks that take an arbitrarily large number of iterations to converge. There are various strategies for mitigating this, such as strategies for reporting overly conservative fixpoints early if iteration takes too long, but the need for and viability of these has not been investigated yet.
2. **Confusing diagnostics** : In the simplest case, the diagnostics from this pass take the form "you transferred `x` to another thread _here_, and then tried to access `x` _here_. There's dataflow from the former of those places to the latter, so hey, that's a race". In the more complex case, the value accessed could appear to be very different from the value transferred. A long sequence of `x0` could alias `x1`, `x1` at one point had a field assigned to `x2`, `x3` also had a field assigned to `x2`, .... terminating in some `x<n>` for large `n` could yield a diagnostic saying `x0` was transferred and `x<n>` was accessed. Leaving the programmer to think: "what the heck do these two values have to do with each other?". Answering that question in diagnostics ergonomically is an interesting problem.
3. **Eventual desire for fancier function types** : The current implementation only has one function signature allowed with respect to the regions of its non-sendable args and results: all arguments including `self` come from a single region, that region cannot be transferred away, and results also inhabit that region. For various reasons, I believe this to be the best option if it's to be the only available option, but there are many increases in complexity of signatures that could be made available. The "transferring args" and "fresh result" sections of the proposal touch on a couple of these, but there are many more: for example, two non-sendable arguments that are both transferred away, but are not assumed to come from distinct regions at the callsite, i.e. they could alias or reference each other. If `iso` fields get added into the mix, then there's a real explosion of possible function signatures. I'm listing this as a catch because adoption of this regions mechanism will almost certainly lead to programmers wanting some or all of these fancier function signatures features, which would add language complexity.

thanks for you continued engagement with this proposal, and let me know any further info or opinions I can provide!

---

<div class="post-metadata">

**Author:** ![wadetregaskis](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/wadetregaskis/32/8494_2.png) [@wadetregaskis](https://forums.swift.org/u/wadetregaskis)\
**Post date:** [August 6, 2023, 12:01am UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/7 "2023-08-06T00:01:37Z")

</div>

> [@JTurcotti](#):
>
> Here are three potential "catches" (roughly in decreasing order of importance)…

Thanks for all these details.

It sounds like these caveats are best judged through actual experience. There's plenty of precedence for algorithms in the compiler that can be pathologically poor-performing (most obviously type inference), and likewise for diagnostics. The key question is whether it falls within the "good enough" range.

Possibly compiler / core team members can estimate that. Or maybe it needs some real demonstrations using real-world example apps and libraries. I can't say, myself, but I look forward to seeing what others think.

---

<div class="post-metadata">

**Author:** ![FranzBusch](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/franzbusch/32/19447_2.png) [@FranzBusch](https://forums.swift.org/u/FranzBusch)\
**Post date:** [August 7, 2023, 1:25pm UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/8 "2023-08-07T13:25:34Z")

</div>

Gave the proposal a very detailed read now and have some questions/suggestions. First, off I think the underlying problem that we are trying to solve here is very important and it boils down to the same problem that `Rust` is solving with [Sync/Send traits](https://doc.rust-lang.org/beta/nomicon/send-and-sync.html).

Right now `Sendable` implies that a type can be safely accessed from multiple tasks/threads at a time. This marker protocol is then also further added to closures through the `@Sendable` annotation. I am not yet convinced that just inferring from the usage of an object if it can be safely _transferred_ across isolation domains/tasks. However, I think the approach of usage analysis is definitely part of the solution to figure out when something is _transferred_ and produce the correct diagnostic.

Personally, I think we should introduce the same distinction of `Sync` and `Send` from Rust to Swift and allow developers to annotate their types and closures correctly. This would solve some of the problems we have seen where we really don't want to make types `Sendable` but we want people to freely transfer those types across tasks. One motivating example of why distinct marker protocols make sense is that we have been going around and marking a bunch of things as explicitly **not Sendable** by using an _unavailable_ extension. We **must never** transfer those types across tasks/isolation domains since the developer clearly marked them as not safe. However, we only marked them this way since we do not have this distinction between `Sync` and `Send`.  
Another thing with the current pitch is that it doesn't state how this works across module. In a single module, we are currently inferring `Sendable` conformance based on the contents of a type. However, we do not make this Public API. With the proposed _transfer_ pitch we should IMO follow the same rules where we cannot assume that types can be transferred from other modules without the developers specifically opting into this.

> Additionally, to achieve the desired level of expressivity, it will be necessary to _remove_ the source-level `@Sendable` annotation on the types of the arguments to these functions. Without the introduction of the `SendNonSendable` pass, race-freedom is attained for functions such as `Task.detached` only by ensuring they only take `@Sendable` args. This prevents entirely the passing of closures that capture non-sendable values to these functions (as such closures are necessarily `@Sendable` )

I think this section clearly outlines why we actually want a separate marker protocol and annotation for `transferring` values. Right now `@Sendable` is used for both closures that might be accessed from multiple tasks and for closures that are only invoked once but might be transferred to a new isolation domain before being invoked.

---

<div class="post-metadata">

**Author:** ![dnadoba](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/dnadoba/32/11438_2.png) [@dnadoba](https://forums.swift.org/u/dnadoba)\
**Post date:** [August 7, 2023, 5:18pm UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/9 "2023-08-07T17:18:19Z")

</div>

I'm with @FranzBusch and a bit worried about which changes are non-API breaking across modules once this has landed.

In addition, the proposal doesn't really mention how it treats Sendable arguments and inout arguments but only really talks about non-sendable arguments and the rules around them. Would this example be rejected:

```swift
class Memory {
    var bytes: [UInt8]
    init(pool: inout [Memory]) {
        self.bytes = []
        pool.append(self)
    }
}

var pool: [Memory] = []
let x1 = Memory(pool: &pool)
Task {
    print(x1.bytes)
}
// data race
pool[0].bytes.append(0)

```

---

<div class="post-metadata">

**Author:** ![JTurcotti](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jturcotti/32/45226_2.png) [@JTurcotti](https://forums.swift.org/u/JTurcotti)\
**Post date:** [August 8, 2023, 12:28am UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/11 "2023-08-08T00:28:31Z")

</div>

Hi @FranzBusch, thanks for the detailed read! Below are my summaries of your concerns (please correct me if misinterpreted), and my responses.

At a high level, I believe it's worth clarifying one important descriptive point about this approach. Protocols such as as `Sendable` (which I believe is equivalent to Rust `Sync`) that provide type-level guarantees about the safety of offering aliases of local references to other threads are useful, but this `SendNonSendable` pass is meant to be a catch-all for allowing concurrent sharing of values for which no type-level information alone suffices to conclude the sharing is safe. Onto more specific responses:

## Viability of usage analysis

### Concern:

**Summary** : Should we rely on analyzing the usage of an object to determine if it's safe to send?  
**Quote:**

> [@FranzBusch](#):
>
> I am not yet convinced that just inferring from the usage of an object if it can be safely _transferred_ across isolation domains/tasks.

### Response:

**Summary** : the analysis is based on usage not type - I believe this is valuable in addition to type-based analysis like protocol conformance; the analysis is intended to be sound but not complete

**Longer form** : To be very clear, this is a conservative analysis. Let's say we reach a point where a local reference `x` is sent to another isolation domain. We want to determine whether this send is safe or not. By safe, we mean that there is no possible program execution resulting in a race over data accessed through `x`. The `SendNonSendable` pass does not guarantee that sends it reports as unsafe indeed can yield such a race. It instead uses static analysis to compute a _region_ including all values that could be accessed through `x` at the point of the send. Namely, this transitively includes all possible aliases and references of `x`. This region is then marked as _transferred_ at that program point. The send is declared safe iff dataflow analysis determines no values in that region are accessed after that program point. This analysis has nothing to do with the type of `x` or anything else in its region. This analysis can have false positives, it cannot have false negatives. In this sense, "type-independent inference from the usage of an object" very much _can_ determine it can be safely transferred across isolation domains/tasks. It cannot determine that a usage is _definitely unsafe_, but that's par for the course.

Let's say we scrapped even the `Sendable` protocol, and solely relied on this flow-sensitive, region-based analysis in the `SendNonSendable` pass. You'd have a somewhat annoying world, in which even very surely non-mutable types like integers would need unsafe copying functions to be sent to other threads then read again. But it wouldn't be an unusable world: any functions that typecheck in current Swift and don't send values to other isolations would still typecheck, and when sends were inserted you'd just have to be very careful to only send data you really don't need in the sending thread again. This is a purely usage-based way of achieving safe concurrency, and it would work.

`Sendable` steps in and makes this world much better because now, everything from primitive types like `Int`, to more complex types like `[Int]`, and internally-synchronized types like actors and unsafe `Sendable` user-defined types can be sent to other threads and still used by the sending thread. These `Sendable` types are types whose values are _always safe_ to send between threads because even concurrent access after the send would not yield a race. Values of other types are not _always_ safe to send, so usage analysis like the `SendNonSendable` pass steps in to ensure concurrent access after the send does not actually occur.

Swift with both the `Sendable` protocol and the `SendNonSendable` pass is thus a hybrid world in which there are two ways to safely send a value to another thread: inspect its type, and realize it's `Sendable` and therefore safe to concurrently access after the send; or inspect its usage, and realize it _will not_ be concurrently accessed after the send. This is flexible and ergonomic, and I believe that offering both type-based safe sending and usage-based safe sending is a valuable approach.

## Comparison to Rust Sync/Send

### Concern:

**Summary** : Should we just lift `Sync/Send` from Rust to Swift as closely as we can instead?  
**Quote** :

> [@FranzBusch](#):
>
> Personally, I think we should introduce the same distinction of `Sync` and `Send` from Rust to Swift and allow developers to annotate their types and closures correctly.

### Response:

**Summary** : I think `SendNonSendable` is more expressive than a lifting of Sync/Send to Swift, but it's an orthogonal feature.

**Longer form** : I believe that the usefulness of Sync/Send in Rust, and the extent to which it's the "right" solution in their context, is directly tied to the nature of their brand of linearity: pervasive single ownership. As discussed a bit above in my response to @wadetregaskis, allowing move-only types to be safely sent between threads as long as the send is treated as a consume would be safe. Since Rust's brand of linearity closely resembles move-only types in Swift, Sync/Send would be a useful distinction for move-only types. `Sendable` would be Sync, and deeply move-only would be Send. These deeply move-only "Sync" types would be safe to send because their move-only nature would prevent aliases being available to the sender after the send, and the "deeply" caveat would ensure that no references accessible from the shared type are aliased in the sending context either. (deeply meaning that any stored properties of the type must also be deeply move-only)

To summarize, I believe that the truest way to lift Sync/Send to Swift would be to make only move-only types be able to conform to the equivalent of Send, and only if they are deeply move-only. My response to @wadetregaskis now nearly directly applies: we could do this, but it would inhibit natural programming patterns, and many of the examples of code that it would allow to typecheck already typecheck with `SendNonSendable`. It has the downsides of requiring more programmer-provided annotation, and of making move-only more prevalent in general - which makes Swift code more complex. For these reasons `SendNonSendable` seems more appealing to me than co-opting the ownership semantics of move-only to make more patterns of concurrency safe. Let me know what you think though!

Finally, on this point, it's worth addressing that if we wanted to add something like a Swift `Transferrable` protocol that is to Rust `Send` what Swift `Sendable` is to Rust `Sync`, and that is only applicable to deeply move-only types as mentioned above, we could! I have various personal reasons to think it's a bad idea, but it would play just as nicely with the `SendNonSendable` pass as `Sendable` currently does: values of `Transferrable` type would simply be ignored by the pass just like values of `Sendable` type currently are.

## API impacts

### Concern:

**Summary** : Does this yield issues with the ability of API designers to control downstream concurrent usage of their types?  
**Quote** :

> [@FranzBusch](#):
>
> With the proposed _transfer_ pitch we should IMO follow the same rules where we cannot assume that types can be transferred from other modules without the developers specifically opting into this.

### Response:

Please adjust my interpretation of your concern if it's off, but I believe that you're imagining a developer who wants to expose a library type `T` with no intention that it ever be used outside of the isolation domain it was defined in (in a sense, never "used with concurrency"). If the reason that the developer wanted this was that they could not guarantee that values of the type could be concurrently accessed without yielding races (in Rust: not `Sync`), or that they could not guarantee that values of the type could be transferred out of their original domain without yielding races through ancillary references accessible _through_ the value (in rust: not `Send`) , then there's no issue with the `SendNonSendable` pass stepping in and letting downstream users of `T` communicate values of its type across isolations. This is because the nature of the pass, combining regions and flow-sensitivity, ensures conservatively that no values that could possibly alias or be references by the sent value are used after the send. There is thus no concern in sending values of type `T` even if the implementer of `T` never intended for them to be sent.

If you're envisioning other reasons that an API designer could want to ensure that no values of a type `T` they provide are ever accessed outside the isolation domain they're designed in, then that could be a useful feature to provide - e.g. a truly `NonTransferrable` protocol or equivalent unavailability - but I'd argue it's once again orthogonal to the `SendNonSendable` pass. I'd love to hear more about the specific situations you're imagining in which a developer would want this feature though, so we could prioritize whether bundling it with `SendNonSendable` is necessary.

## Conclusion

I hope I didn't mischaracterize any of your concerns. Thanks for your time in reading this proposal. I think we all agree that something that increases the expressiveness of Swift Concurrency beyond the current strict `Sendable` requirement is warranted, so hopefully as a community we can converge on what that right thing is!

---

<div class="post-metadata">

**Author:** ![JTurcotti](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jturcotti/32/45226_2.png) [@JTurcotti](https://forums.swift.org/u/JTurcotti)\
**Post date:** [August 8, 2023, 12:38am UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/12 "2023-08-08T00:38:13Z")

</div>

Hi @dnadoba, I attempted to address concerns about API design in my response to @FranzBusch, but let me know if any points there are still unresolved. Long story shot is I don't believe that this feature provides any new avenues for surprising API breaks: whether or not sending a value across isolations is independent of anything in the definition of its type, so changing the upstream definition of its type cannot break usages.

The quick answer to "how does this pass treat `Sendable` arguments" (and `Sendable` values in general) is that it doesn't. `Sendable` values are totally ignored by this pass: they don't get tracked in regions, they don't get "transferred", they don't get checked to ensure they haven't been transferred at points they're sent across isolations. As to how `inout` arguments are treated, I have not thought hard enough to say this with 100% certainty, but I believe that treating them as non-sendable regardless of their type would be a safe approach. The issue is preventing them from escaping into stored properties, global variables, or other isolations such as tasks, which the current tracking for non-sendable values in `SendNonSendable` does. It would be essentially treating them as instances of a box class instead of as their true type. But it's worth thinking more about this, so thinks for bringing it to my attention!

As to the specific code you shared, I believe that it does not illustrate a race as written? possibly due to a typo in which the `pool` parameter to `Memory.init` being unused? Feel free to explain it to me more though to help me understand the issue. Looking at the top-level code and ignoring everything about the `Memory` class except that it's non-sendable, the example would not be accepted by `SendNonSendable`. `x1`'s initialization would cause it to be in the same region as `pool`, so transferring `x1` to the task would transfer `pool` too, and the potentially racy access on the last line would yield a diagnostic.

---

<div class="post-metadata">

**Author:** ![wadetregaskis](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/wadetregaskis/32/8494_2.png) [@wadetregaskis](https://forums.swift.org/u/wadetregaskis)\
**Post date:** [August 8, 2023, 12:59am UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/13 "2023-08-08T00:59:12Z")

</div>

> [@JTurcotti](#):
>
> These `Sendable` types are types whose values are _always safe_ to send between threads because even concurrent access after the send would not yield a race. Values of other types are not _always_ safe to send, so usage analysis like the `SendNonSendable` pass steps in to ensure concurrency access after the send does not actually occur.

I really like this framing. Safety is ultimately about how something is _actually_ used, not necessarily an intrinsic aspect of the something's _type_. Or at least it's better that way - so much more flexible.

Having a way to say "this type is _always_ safe" is a great shortcut (or way to override the compiler in cases where it isn't smart enough to determine that automatically), but as you say technically it's just an optimisation.

> [@JTurcotti](#):
>
> If you're envisioning other reasons that an API designer could want to ensure that no values of a type `T` they provide are ever accessed outside the isolation domain they're designed in, then that could be a useful feature to provide - e.g. a truly `NonTransferrable` protocol or equivalent unavailability.

Sounds like it's a `!Sendable`. As in, a ternary - something can be explicitly `Sendable`, explicitly _not_ `Sendable`, or indeterminate (meaning: see how it's actually used, case by case, and figure it out then).

I'm having trouble coming up with an example of something that needs to be explicitly _not_ transferrable in this way, in its immediate incarnation. But perhaps it's an important declaration even if only from a forward-compatibility perspective: maybe type `Foo` happens to be safe to send today, but the owner of that type wants to reserve the right to revoke that sendability in future (and therefore, for modules with resiliency enabled, it can't be sent even now). Today they can do that by simply omitting `Sendable`, but with the pitched mechanism a way to opt out is required.

---

<div class="post-metadata">

**Author:** ![FranzBusch](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/franzbusch/32/19447_2.png) [@FranzBusch](https://forums.swift.org/u/FranzBusch)\
**Post date:** [August 8, 2023, 9:44am UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/14 "2023-08-08T09:44:11Z")

</div>

Thanks for the detailed reply!

> **Summary** : the analysis is based on usage not type - I believe this is valuable in addition to type-based analysis like protocol conformance; the analysis is intended to be sound but not complete

> If you're envisioning other reasons that an API designer could want to ensure that no values of a type `T` they provide are ever accessed outside the isolation domain they're designed in, then that could be a useful feature to provide - e.g. a truly `NonTransferrable` protocol or equivalent unavailability - but I'd argue it's once again orthogonal to the `SendNonSendable` pass. I'd love to hear more about the specific situations you're imagining in which a developer would want this feature though, so we could prioritize whether bundling it with `SendNonSendable` is necessary.

I completely agree that usage based analysis is necessary and I am not opposed to adding this. I was always worried that the only way to solve some of the current `Sendable` constraints where you want to say that a type is not `Sendable` but \_transferrable`is by adopting`~Copyable` types. However, those types currently come with severe limitations and can't be applied to everything.  
The only concern that I have left on the usage base analysis approach is that there should be a way for a developer to opt their type out of participating in this. There might be some truly weird types out there that rely on only being called on a single thread ever. Right now we suggest developers to mark those types like this.

```swift
@available(*, unavailable)
extension SomeType: Sendable { }

```

However, maybe this is also just not a problem since the current non-`Sendable` doesn't guarantee that a value is ever going to called on the same thread ever just that it is only called from a single thread at a time and that it cannot pass isolation domains. Maybe we should just add a section about this discussion in Future directions/Alternatives considered.

> Additionally, to achieve the desired level of expressivity, it will be necessary to _remove_ the source-level `@Sendable` annotation on the types of the arguments to these functions. Without the introduction of the `SendNonSendable` pass, race-freedom is attained for functions such as `Task.detached` only by ensuring they only take `@Sendable` args. This prevents entirely the passing of closures that capture non-sendable values to these functions (as such closures are necessarily `@Sendable` ). By implementing the `SendNonSendable` pass, sendable closures will still be able to be passed freely to these functions, but non-sendable closures will not be outright banned - rather they will be subject to the same flow-sensitive region-base checking as all other non-sendable values passed to isolation-crossing calls: if their region has not been transferred the call will be allowed, and otherwise it will throw an error. This change (removing `@Sendable` from the argument signatures of these functions) is necessary.

I find this section very interesting and it contains a bunch of complexity. From personal experience most of the `@Sendable` annotations on closures come from the fact that they are run inside a child task or unstructured task in the end. A common pattern is a function like this:

```swift
func withAccessToFoo(_ body: @Sendable (Foo) async -> Void) async {
    let foo = Foo()
    await withTaskGroup(of: Void.self) { group in
        group.addTask { foo.run() }
        group.addTask { body(foo) }

        await group.next()
        group.cancelAll()
    }
}

```

Right now this is requires the `@Sendable` just because `group.addTask` requires `@Sendable`. I expect that in a lot of places we could actually drop the `@Sendable` annotation from methods once we have a way to define that they that the `body` closure is _transferred_. I would love to see capabilities included in the initial implementation which allows users to define such methods without the requirement of `@Sendable`.

> 1. Mark such functions with an annotation such as `@IsolationCrossing` at the source level where they're defined.

How does this play with the current `@_inheritActorContext` attribute? With this attribute `Task.init` inherits the current actor context and would not be isolation crossing.

> The `iso` keyword, when placed on fields, for example as `iso var contents : NonSendable` above, would indicate that instead of tracking the source and target of the reference (here, `self` and `self.contents` ) as necessarily belonging to the same region, they could belong to _different_ regions.

I don't fully see how this can be true. The following example from the pitch can never be safely transfer the `contents` over to the `otherBox` without introducing a data race since the `IndecisiveBox` can be reentrantly called so both the `otherBox` and this current box might have overlapping access to the reference of `contents` unless I am missing something.

```swift
actor IndecisiveBox {
  var contents : NonSendable
  
  func setContents(_ val : NonSendable) {
    contents = val
  }
  
  func swapWithOtherBox(_ otherBox : IndecisiveBox) async {
    let otherContents = await otherBox.contents
    await otherBox.setContents(contents)
    contents = otherContents
  }
}

```

Overall a big fan of this work!

---

<div class="post-metadata">

**Author:** ![dnadoba](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/dnadoba/32/11438_2.png) [@dnadoba](https://forums.swift.org/u/dnadoba)\
**Post date:** [August 8, 2023, 2:00pm UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/15 "2023-08-08T14:00:00Z")

</div>

Yes, you are right, sorry. I have missed an `pool.append(self)` in the init of `Memory`. I have updated my post.

Sounds reasonable to me to treat inout parameters as non-sendable parameters.

---

<div class="post-metadata">

**Author:** ![lukasa](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/lukasa/32/1232_2.png) [@lukasa](https://forums.swift.org/u/lukasa)\
**Post date:** [August 8, 2023, 3:15pm UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/16 "2023-08-08T15:15:02Z")

</div>

> [@JTurcotti](#):
>
> This is because the nature of the pass, combining regions and flow-sensitivity, ensures conservatively that no values that could possibly alias or be references by the sent value are used after the send. There is thus no concern in sending values of type `T` even if the implementer of `T` never intended for them to be sent.

Is that true in the case of cross-module calls? The pitch as written provides 5 ways to expand regions:

> [@](#):
>
> - `let y = x.f`: reading a non-sendable-typed field of `x` yields a value `y` in the same region as `x`
> - `let y = x`: creating an alias `y` yields a value in the same region as `x`
> - `y.f = x`: creating a reference from a non-sendable value `y` to a non-sendable value `x` merges their regions
> - `z = { ... x ... y ...}`: capturing non-sendable values `x` and `y` in a closure merges their regions, and yields a new closure `z` in the same region as `x` and `y`
> - `if/for/while/switch` (control flow): if `x` and `y` are in the same region in any predecessor basic block of a function, then they will be merged into the same region in all successors of that basic block

There are a few things missing here, some of which fit easily and some of which do not.

The first is `with` functions:

```swift
public class RefcountedBuffer {
    private var bytes: UnsafeRawBufferPointer

    deinit { bytes.deallocate() }

    public func withBytes(_ body: (UnsafeRawBufferPointer) -> Void) {
        body(bytes)
    }
}

```

Now it would seem that your 3rd rule ("creating a reference from a non-sendable value `y` to a non-sendable value `x` merges their regions") would apply here. However, the contents of `withBytes` is not available to a caller from a different module, so they can't see where the argument came from.

This isn't a big deal: the conservative approach is to assume that all values given to a closure passed to a method potentially alias the value in question, and so may not be sent if that value is used elsewhere.

What I don't understand clearly is how this works with thread locals, which have no requirement to be Sendable but which can be used after the end of a given function. What is your thinking here?

---

<div class="post-metadata">

**Author:** ![JTurcotti](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jturcotti/32/45226_2.png) [@JTurcotti](https://forums.swift.org/u/JTurcotti)\
**Post date:** [August 8, 2023, 7:30pm UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/17 "2023-08-08T19:30:02Z")

</div>

> [@FranzBusch](#):
>
> Maybe we should just add a section about this discussion in Future directions/Alternatives considered.

done!

> [@FranzBusch](#):
>
> A common pattern is a function like this:

Indeed, `TaskGroup.addTask` is another function that should be modified to not require a `@Sendable` closure. I've updated the proposal to reflect this, but the mentioned solution `@IsolationCrossing` is only the best solution if there is no `transferring` qualifier available on arguments to functions. As discussed in the "Transferring args" section of the proposal, such a qualifier would indicate that instead of the function being expected to preserve the region of that argument, it can transfer it to another isolation domain itself. Placing this qualifier on `Task.init`, `Task.detached`, `MainActor.run`, `TaskGroup.addTask`, and other functions that indeed are not isolation-crossing calls but either transfer their argument internally (like `MainActor.run`) or create a new isolation domain for that argument (`Task.init`) is the appropriate solution.

With this change made (swapping `@Sendable` for `transferring` on the argument to `TaskGroup.addTask`, and also making this change to `withTaskGroup`'s closure argument if needed) the code you provided would typecheck as is, and with the `@Sendable` annotation on the argument removed; provided `Foo` is a `Sendable` type. If not, the first call to `group.addTask` would transfer `foo` so the second call would be an error.

> [@FranzBusch](#):
>
> I don't fully see how this can be true. The following example from the pitch can never be safely transfer the `contents` over to the `otherBox` without introducing a data race since the `IndecisiveBox` can be reentrantly called so both the `otherBox` and this current box might have overlapping access to the reference of `contents` unless I am missing something.

I forgot about reentrancy when drafting that example! Just updated it in the proposal to be safe in the presence of actor reentrancy. `iso` fields are a subtle feature, but very powerful. Unfortunately, they are likely very much out of scope for short term development of this pass, so if you're further interested in what they allow I think [the linked paper](https://www.cs.cornell.edu/andru/papers/gallifrey-types/) really is your best bet!

Thanks for continuing to help me refine this proposal!

---

<div class="post-metadata">

**Author:** ![FranzBusch](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/franzbusch/32/19447_2.png) [@FranzBusch](https://forums.swift.org/u/FranzBusch)\
**Post date:** [August 14, 2023, 9:49am UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/18 "2023-08-14T09:49:07Z")

</div>

We just fixed a Sendable checking hole in the nightly compiler with this PR: [[Concurrency] Diagnose non-sendable 'self' arguments crossing isolation boundaries. by hborla · Pull Request #67730 · apple/swift · GitHub](https://github.com/apple/swift/pull/67730)

The PR was the correct thing to-do but it uncovered a bunch of correct warnings in our code. A common pattern that now produces a warning with strict checking on the latest nightlies looks like this

```swift
actor Foo {
    let stream = AsyncStream<Int>.makeStream().0

    func foo() async {
        for await element in stream { // warning: passing argument of non-sendable type 'inout AsyncStream<Int>.Iterator' outside of actor-isolated context may introduce data races

        }
    }
}

```

This is correct since async sequence iterators are not `Sendable` and we are sending it across an isolation domain here. The proposed `SendNonSendable ` pass here wouldn't solve this since we are still using the iterator after transferring it, right?

It is probably orthogonal to this problem and only solvable with making it possible to consume async sequences on an actor's executor.

---

<div class="post-metadata">

**Author:** ![aggie33](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/aggie33/32/29896_2.png) [@aggie33](https://forums.swift.org/u/aggie33)\
**Post date:** [August 16, 2023, 4:59pm UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/19 "2023-08-16T16:59:32Z")

</div>

Why not just store the `AsyncSequence` instead of the `AsyncSequence.Iterator`?

---

<div class="post-metadata">

**Author:** ![Jon\_Shier](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jon_shier/32/921_2.png) [@Jon\_Shier](https://forums.swift.org/u/Jon_Shier)\
**Post date:** [August 16, 2023, 5:04pm UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/20 "2023-08-16T17:04:21Z")

</div>

`makeStream().0` is the `AsyncSequence`, namely the `AsyncStream`. But the `for await` loop grabs the iterator from that value, apparently crossing async boundaries in the process. (@FranzBusch what boundary is crossed here? Iterator's isolation (default executor?) to actor's?)

---

<div class="post-metadata">

**Author:** ![aggie33](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/aggie33/32/29896_2.png) [@aggie33](https://forums.swift.org/u/aggie33)\
**Post date:** [August 16, 2023, 5:06pm UTC](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566/21 "2023-08-16T17:06:30Z")

</div>

Oh, I see, that is strange. It's not really taking an iterator, it's making a new one with `makeIterator()`. So I wonder what the problem with that is

[Next page](https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566.md?page=2)
