[Pitch] Allow 'nonisolated' to prevent global actor inference

Hello, Swift Evolution!

I and @hborla have drafted a proposal that allows annotating a set of declarations with nonisolated to "cut-off" the global actor inference coming from the conformance list.

For example, in some cases, there is a need to conform to a globally-isolated protocol and keep the overall type non-isolated. Under this proposal, it will be possible for a type to conform to a globally-isolated protocol without inferring the global actor isolation on the overall type.

More specifically, we propose allowing nonisolated to be applied to:

  • Protocols
  • Extensions
  • Classes, structs, enums
  • ... and more!!

Check out the full proposal draft here: swift-evolution/proposals/NNNN-nonisolated-for-global-actor-cutoff.md at nonisolated-for-global-actor-cutoff · simanerush/swift-evolution · GitHub

We welcome editorial suggestions! Feel free to comment on the swift-evolution PR here: Add a proposal to allow `nonisolated` to prevent global actor inference. by simanerush · Pull Request #2560 · swiftlang/swift-evolution · GitHub

We're looking forward to hearing your feedback!

20 Likes

Nice proposal overall, I look forward to this cleanup in the language.

The "remove global actor" sample snippet has some mistakes that made would make it not compile, here's a fix proposal PR: Fix some code snippets in NNNN-nonisolated-for-global-actor-cutoff.md by ktoso · Pull Request #1 · simanerush/swift-evolution · GitHub (the executor has the enqueue, not the actor type)

Points 1-3 are very clear and good.

I understand the intent and semantics of point 4. Mutable Sendable storage of Sendable value types but the wording is a bit confusing. Can we polish it up a bit? I feel like we're mixing things up a bit with the within/outside module part. The shown type S is implicitly Sendable but that's within module, we should probably call this out. Can we clarify if we have to spell out the nonisolated for it to be seen as safe cross module, or if this is already the existing rule and we just loosen it up across module?

The bit

Therefore, synchronized access to x in the example above is safe.

Feels like it's inverted as well? In this proposal we're basically saying that more kinds of **un-**synchronized access are okey, rather than synchronized (using actors or locks).

Rule 5 LGTM as-is :slight_smile:

The Restrictions section could use a "why" sentence next to every example rather than just that we're banning these situations. Proposals often serve as learning material so we should take that chance to educate about the "why"'s here. We might also want to call out that there's nonisolated(unsafe) if the necessity were to arrise somewhere (I see folks often unaware of that escape-hatch).

Overall looks good to me! Just those few nitpicks. Semantics look good and should help prevent proliferation of the remove actor isolation "trick" by replacing it with a proper spelling.

2 Likes

For clarity, is this pitch proposing to make it possible to explicitly spell nonisolated var x: Int here or to infer that it is nonisolated?

There are many Sendable types that wrap an underlying Sendable representation: for instance, we recommend that users do this to add new conformances to types they don't own. Is the upshot of this pitch that, for maximum ergonomics, users ought to go back and annotate their types' members pervasively with nonisolated if it isn't to be inferred? (Are there performance or other drawbacks of doing so?) Are there ABI compatibility considerations to writing nonisolated (e.g., if the type is @frozen)?


Regarding the final part which actually seems to be the main crux of the pitch, given nonisolated struct S { ... } and extension S: GloballyIsolatedProtocol { ... }—which rule "wins" for members defined in the extension?

1 Like

Thank you! I've updated the proposal🙂

1 Like

Under point three I'm having some trouble properly understanding what this means:

Because MyClass is does not conform to Sendable, the compiler guarantees mutually exclusive access to references of MyClass instance. nonisolated on methods and properties of non-Sendable types can be safely called from any isolation domain because the base instance can only be accessed by one isolation domain at a time.

Isn't it the case that non-Sendable types can't be accessed safely from multiple isolation domains due to the fact they don't do anything to protect their mutable state from data races?

I'm also not quite sure what the "base instance" refers to in that example. Could it be that the code sample isn't fully complete?

Nit: there's also a typo "is does not conform" should probably be "does not conform"

1 Like

That's right - non-Sendable types cannot be accessed from multiple isolation domains at once. The text is saying that they can be used from any single isolation domain. The nonisolated on a method or property doesn't have any impact on how many isolation domains can reference the self value, it only means that once you have a reference to the self value, you can safely call the nonisolated method/property. We'll clarify this in the proposal text.

5 Likes

Thank you :slight_smile: I think it might be helpful to clarify the text a bit with code samples of what's intended to be communicated; for me that's usually a lot easier to digest since proposal texts like this one can be very information dense.

2 Likes

Agreed. This was my biggest confusion with the proposal text — what’s the behavioral difference between declaring a property of a non-Sendable class as nonisolated and not doing so?

I'm not sure I totally understand the clarification. Specifically, this example—

This text implies that, currently, print(s.x) is not OK (where x is not explicitly annotated as nonisolated, since of course that would require the pitched feature to be allowed). However, it is?

// `_value` is the public storage of `Int`, a `Sendable` type.
actor MyActor {
  func test(s: Int) {
    print(s._value) // Allowed on nightly with complete strict concurrency
  }
}
1 Like