5
5
* Review Manager: [ Doug Gregor] ( https://github.com/DougGregor )
6
6
* Status: ** Active review (March 27...April 9, 2024)**
7
7
* 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 )
8
9
* 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 ) )
9
10
10
11
[ SE-0316 ] : https://github.com/apple/swift-evolution/blob/main/proposals/0316-global-actors.md
11
12
[ SE-0392 ] : https://github.com/apple/swift-evolution/blob/main/proposals/0392-custom-actor-executors.md
12
13
[ isolated-captures ] : https://forums.swift.org/t/closure-isolation-control/70378
13
14
[ 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
14
17
15
18
## Introduction
16
19
@@ -213,12 +216,6 @@ declare a function *entity* as having `@isolated(any)` isolation,
213
216
because Swift needs to know what the actual isolation is, and
214
217
` @isolated(any) ` does not provide a rule for that.
215
218
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
-
222
219
### Conversions
223
220
224
221
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
311
308
statically unknown, calls to it typically cross an isolation boundary.
312
309
This means that the call must be ` await ` ed even if the function is
313
310
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.
315
336
316
337
In order for a call to an ` @isolated(any) ` function to be treated as
317
338
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
320
341
require the caller to be declared with value-specific isolation. It
321
342
is currently not possible for a local function or closure to be
322
343
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) `
324
345
should interact with possible future language support for functions
325
346
that are explicitly isolated to a captured value. In order to
326
347
present these rules, this proposal uses the syntax currently proposed
@@ -331,7 +352,7 @@ that pitch. Accepting this proposal will leave most of this
331
352
section "suspended" until a feature with a similar effect is added
332
353
to the language.
333
354
334
- [ ^ 4 ] : Technically, it is possible to achieve this effect in Swift
355
+ [ ^ 6 ] : Technically, it is possible to achieve this effect in Swift
335
356
today in a way that Swift could conceivably look through: the caller
336
357
could be a closure with an ` isolated ` parameter, and that closure
337
358
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,
641
662
but outside of a task, arbitrary things can be isolated without Swift
642
663
knowing about them. It is also needlessly restrictive, because there
643
664
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
645
666
intuitive but more closely matches the safety properties that static
646
667
isolation checking tests for. It implies that ` assumeIsolated(nil) `
647
668
should always succeed. This is notably good enough for ` @isolated(any) ` :
@@ -650,7 +671,7 @@ since `assumeIsolated` is a synchronous function, only synchronous
650
671
synchronous non-isolated function always runs immediately without
651
672
changing the current isolation.
652
673
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
654
675
could conceivably have important semantic restrictions against doing
655
676
certain operations in its isolated code. Of course, such an actor should
656
677
generally not be calling arbitrary functions that are handed to it.
@@ -757,7 +778,85 @@ added this proposal first.
757
778
758
779
## Alternatives considered
759
780
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.)
761
860
762
861
## Acknowledgments
763
862
0 commit comments