Skip to content

Commit 6c76be4

Browse files
authored
Revise SE-0431 to remove the @Sendable implication. (#2429)
Also, discuss some of the syntax alternatives we explored.
1 parent 49f5b77 commit 6c76be4

File tree

1 file changed

+111
-12
lines changed

1 file changed

+111
-12
lines changed

proposals/0431-isolated-any-functions.md

Lines changed: 111 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
* Review Manager: [Doug Gregor](https://github.com/DougGregor)
66
* Status: **Active review (March 27...April 9, 2024)**
77
* Implementation: [apple/swift#71433](https://github.com/apple/swift/pull/71433), [apple/swift#71574](https://github.com/apple/swift/pull/71574), available on `main` with `-enable-experimental-feature IsolatedAny`.
8+
* Previous revision: [1](https://github.com/apple/swift-evolution/blob/b35498bf6f198477be50809c0fec3944259e86d0/proposals/0431-isolated-any-functions.md)
89
* Review: ([pitch](https://forums.swift.org/t/isolated-any-function-types/70562))([review](https://forums.swift.org/t/se-0431-isolated-any-function-types/70939))
910

1011
[SE-0316]: https://github.com/apple/swift-evolution/blob/main/proposals/0316-global-actors.md
1112
[SE-0392]: https://github.com/apple/swift-evolution/blob/main/proposals/0392-custom-actor-executors.md
1213
[isolated-captures]: https://forums.swift.org/t/closure-isolation-control/70378
1314
[generalized-isolation]: https://github.com/apple/swift-evolution/blob/main/proposals/0420-inheritance-of-actor-isolation.md#generalized-isolation-checking
15+
[regions]: https://github.com/apple/swift-evolution/blob/main/proposals/0414-region-based-isolation.md
16+
[region-transfers]: https://github.com/apple/swift-evolution/blob/main/proposals/0430-transferring-parameters-and-results.md
1417

1518
## Introduction
1619

@@ -213,12 +216,6 @@ declare a function *entity* as having `@isolated(any)` isolation,
213216
because Swift needs to know what the actual isolation is, and
214217
`@isolated(any)` does not provide a rule for that.
215218

216-
To reduce the number of attributes necessary in typical uses,
217-
`@isolated(any)` implies `@Sendable`. It is generally not useful
218-
to use `@isolated(any)` on a non-`Sendable` function because a
219-
non-`Sendable` function must be isolated to the current concurrent
220-
context.
221-
222219
### Conversions
223220

224221
Let `F` and `G` be function types, and let `F'` and `G'` be the corresponding
@@ -311,7 +308,31 @@ Since the isolation of an `@isolated(any)` function value is
311308
statically unknown, calls to it typically cross an isolation boundary.
312309
This means that the call must be `await`ed even if the function is
313310
synchronous, and the arguments and result must satisfy the usual
314-
sendability restrictions for cross-isolation calls.
311+
sendability restrictions for cross-isolation calls. The function
312+
value itself must satisfy a slightly less restrictive rule: it must
313+
be a sendable value only if it is `async` and the current
314+
context is not statically known to be non-isolated.[^4]
315+
316+
[^4]: The reasoning here is as follows. All actor-isolated functions
317+
are inherently `Sendable` because they will only use their captures from
318+
an isolated context.[^5] There is only a data-race risk for the
319+
captures of a non-`Sendable` `@isolated(any)` function in the case
320+
where the function is dynamically non-isolated. The sendability
321+
restrictions therefore boil down to the same restrictions we would
322+
impose on calling a non-isolated function. A call to a non-isolated
323+
function never crosses an isolation boundary if the function is
324+
synchronous or if the current context is non-isolated.
325+
326+
[^5]: Sending an isolated function value may cause its captures to be
327+
*destroyed* in a different context from the function's formal isolation.
328+
Swift pervasively assumes this is okay: copies of non-`Sendable` values
329+
must still be managed in a thread-safe manner. This is a significant
330+
departure from Rust, where non-`Send` values cannot necessarily be safely
331+
managed concurrently, and it means that `Sendable` is not sufficient
332+
to enable optimizations like non-atomic reference counting. Swift
333+
accepts this in exchange for being more permissive, as long as the code
334+
avoids "user-visible" data races. Note that this assumption is not new
335+
to this proposal.
315336

316337
In order for a call to an `@isolated(any)` function to be treated as
317338
not crossing an isolation boundary, the caller must be known to have
@@ -320,7 +341,7 @@ the same isolation as the function. Since the isolation of an
320341
require the caller to be declared with value-specific isolation. It
321342
is currently not possible for a local function or closure to be
322343
isolated to a specific value that isn't already the isolation of the
323-
current context.[^4] The following rules lay out how `@isolated(any)`
344+
current context.[^6] The following rules lay out how `@isolated(any)`
324345
should interact with possible future language support for functions
325346
that are explicitly isolated to a captured value. In order to
326347
present these rules, this proposal uses the syntax currently proposed
@@ -331,7 +352,7 @@ that pitch. Accepting this proposal will leave most of this
331352
section "suspended" until a feature with a similar effect is added
332353
to the language.
333354

334-
[^4]: Technically, it is possible to achieve this effect in Swift
355+
[^6]: Technically, it is possible to achieve this effect in Swift
335356
today in a way that Swift could conceivably look through: the caller
336357
could be a closure with an `isolated` parameter, and that closure
337358
could be called with an expression like `fn.isolation` as the arugment.
@@ -641,7 +662,7 @@ Swift's concurrency runtime does track the current isolation of a task,
641662
but outside of a task, arbitrary things can be isolated without Swift
642663
knowing about them. It is also needlessly restrictive, because there
643664
is nothing that is unsafe to do in an isolated context that would be
644-
safe if done in a non-isolated context.[^5] The second rule is less
665+
safe if done in a non-isolated context.[^7] The second rule is less
645666
intuitive but more closely matches the safety properties that static
646667
isolation checking tests for. It implies that `assumeIsolated(nil)`
647668
should always succeed. This is notably good enough for `@isolated(any)`:
@@ -650,7 +671,7 @@ since `assumeIsolated` is a synchronous function, only synchronous
650671
synchronous non-isolated function always runs immediately without
651672
changing the current isolation.
652673

653-
[^5]: As far as data-race safety goes, at least. A specific actor
674+
[^7]: As far as data-race safety goes, at least. A specific actor
654675
could conceivably have important semantic restrictions against doing
655676
certain operations in its isolated code. Of course, such an actor should
656677
generally not be calling arbitrary functions that are handed to it.
@@ -757,7 +778,85 @@ added this proposal first.
757778

758779
## Alternatives considered
759780

760-
(to be expanded)
781+
### Other spellings
782+
783+
`isolated` and `nonisolated` are used as bare-word modifiers in several
784+
places already in Swift: you can declare a parameter as `isolated`, and
785+
you can declare methods and properties as `nonisolated`. Using `@isolated`
786+
as a function type attribute therefore risks confusion about whether
787+
`isolated` should be written with an `@` sign.
788+
789+
One alternative would be to drop the `@` sign and spell these function
790+
types as e.g. `isolated(any) () -> ()`. However, this comes with its own
791+
problems. Modifiers typically affect a specific entity without changing
792+
its type; for example, the `weak` modifier makes a variable or property
793+
a weak reference, but the type of that reference is unchanged (although
794+
it is required to be optional). This wouldn't be too confusing if
795+
modifiers and types were written in fundamentally different places, but
796+
it's expected that `@isolated(any)` will usually be used on parameter
797+
functions, and parameter modifiers are written immediately adjacent to
798+
the parameter type. As a result, removing the `@` would create this
799+
unfortunate situation:
800+
801+
```swift
802+
// This means `foo` is isolated to the actor passed in as `actor`.
803+
func foo(actor: isolated MyActor) {}
804+
805+
// This means `operation` is a value of isolated(any) function type;
806+
// it has no impact on the isolation of `bar`.
807+
func bar(operation: isolated(any) () -> ())
808+
```
809+
810+
It is better to preserve the current rule that type modifiers are
811+
written with an `@` sign.
812+
813+
Another alternative would be to not spell the attribute `@isolated(any)`.
814+
For example, it could be spelled `@anyIsolated` or `@dynamicallyIsolated`.
815+
The spelling `@isolated(any)` was chosen because there's an expectation
816+
that this will be one of a family of related isolation-specifying
817+
attributes. For example, if Swift wanted to make it easier to inherit
818+
actor isolation from one's caller, it could add an `@isolated(caller)`
819+
attribute. Another example is the `@isolated(to:)` future direction
820+
listed above. There's merit in having these attributes be closely
821+
related in spelling. Using a common `Isolated` suffix could serve as
822+
that connection, but in the author's opinion, `@isolated` is much
823+
clearer.
824+
825+
If programmers do end up confused about when to use `@` with `isolated`,
826+
it should be relatively straightforward to provide a good compiler
827+
experience that corrects misuses.
828+
829+
### Implying `@Sendable`
830+
831+
An earlier version of this proposal made `@isolated(any)` imply `@Sendable`.
832+
The logic behind this implication was that `@isolated(any)` is only
833+
really useful if the function is going to be passed to a different
834+
concurrent context. If a function cannot be passed to a different
835+
concurrent context, the reasoning goes, there's really no point in
836+
it carrying its isolation dynamically, because it can only be used
837+
if that isolation is compatible with the current context. There's
838+
therefore no reason not to eliminate the redundant `@Sendable` attribute.
839+
840+
However, this logic subtly misunderstands the meaning of `Sendable`
841+
in a world with [region-based isolation][regions]. A type conforming
842+
to `Sendable` means that its values are intrinsically thread-safe and
843+
can be used from multiple concurrent contexts *concurrently*.
844+
Values of non-`Sendable` type are still safe to use from different
845+
concurrent contexts as long as those uses are well-ordered: if the
846+
value is properly [transferred][region-transfers] between contexts,
847+
everything is fine. Given that, it is sensible for a non-`Sendable`
848+
function to be `@isolated(any)`: if the function can be transferred
849+
to a different concurrent context, it's still useful for it to carry
850+
its isolation dynamically.
851+
852+
In particular, something like a task-creation function ought to declare
853+
the initial task function as a non-`@Sendable` but still transferrable
854+
`@isolated(any)` function. This permits closures passed in to capture
855+
non-`Sendable` state as long as that state can be transferred into the
856+
closure. (Ideally, the initial task function would then be able to
857+
transfer that captured state out of the closure. However, this would
858+
require the compiler to understand that the task function is only
859+
called once.)
761860

762861
## Acknowledgments
763862

0 commit comments

Comments
 (0)