Assigning to a `let` instance variable in nonisolated init

Hi,

I've built our app with Xcode 15.3 (so Swift 5.10) and got a new warning that intrigued me. Here's a simplified version of the code that reproduces the warning.

protocol Bar {}

@MainActor final class Foo {
  nonisolated private let bar: any Bar

  nonisolated init(bar: any Bar) {
    self.bar = bar // Main actor-isolated property 'bar' can not be mutated from a non-isolated context; this is an error in Swift 6
  }
}

My understanding was that assigning to a let instance variable in a nonisolated init should be fine. Is that because it might cross concurrency domains? The error message seems a bit misleading though, especially as it did not change as I made bar nonisolated.
I realized that making Bar sendable by changing protocol Bar {} to protocol Bar: Sendable {} makes the warning disappear.

Thanks

3 Likes

Yes, the reason why this crosses an isolation boundary is because you're storing the value bar into @MainActor isolated state. As soon as you do that, the value is accessible from the @MainActor, so it means the argument that you passed into the nonisolated init shouldn't be accessed from the caller anymore if the caller isn't on the main actor.

Yes, if Bar were Sendable, this would be okay because you get a promise that the type is thread safe, so both the MainActor and the caller's isolation domain can safely access the value concurrently.

I agree that the error message could definitely be clearer about what's happening here.

Marking let bar as nonisolated also has some confusing behavior in Swift 5.10 - even if you do that, the compiler will still apply a Sendable check when you access bar in an actor isolated context, such as in an isolated method of Foo, so you'll still get warnings because any Bar is not Sendable. I've already tweaked this behavior in [Concurrency] `nonisolated` can only be applied to actor properties with `Sendable` type. by hborla · Pull Request #70909 · apple/swift · GitHub

2 Likes

Can you explain why this is? Because to me it looks like the bar stored property is not isolated - to the main actor or anywhere else - because it says it's not, with nonisolated…?

Ah sorry, my PR description above has an explanation, but I'll repeat/expand on it here.

In compiler versions <=Swift 5.10, the compiler allows you to write nonisolated on any let inside of an actor or actor-isolated type, but if the property type is not Sendable, it's still not safe to access from an arbitrary isolation domain because, well, it's not thread safe. For example, a class can be declared with let but still have shared mutable state that could be accessed concurrently and cause a data race. So, if you access bar from a nonisolated context, you get a warning because the compiler had ad-hoc Sendable checks for actor properties marked as nonisolated:

protocol Bar {}

@MainActor final class Foo {
  nonisolated private let bar: any Bar

  init(bar: any Bar) {
    self.bar = bar
  }

  nonisolated func test() {
    print(bar) // warning: Non-sendable type 'any Bar' in asynchronous access to nonisolated property 'bar' cannot cross actor boundary
  }
}

nonisolated on a non-Sendable property isn't helpful because you still cannot safely access the value across isolation boundaries, so the compiler should not let you write this at all. My PR above instead diagnoses this at the point when you apply nonisolated, so in the Swift 6.0 nightly toolchains, this diagnoses

17 │ @MainActor final class Foo {
18 │   nonisolated private let bar: any Bar
   │   ╰─ warning: 'nonisolated' can not be applied to variable with non-'Sendable' type 'any Bar'; this is an error in the Swift 6 language mode
19 │ 

Does this explanation help clarify the behavior?

4 Likes

I see, so nonisolated is intended to mean "this can be directly accessed from any and all isolation domains" but doesn't excuse the variable from being safe to do so.

I guess that makes sense, then, given the existence of nonisolated(unsafe) (in the same vein as @unchecked Sendable as a way of saying "it's either safe [and you just don't understand why] or I don't care about safety".