Strict Concurrency: False Positives?

I'm pretty sure a @MainActor annotated closure should be implicitly Sendable, but the following generates a warning:

struct Box: Sendable {
  // Stored property 'action' of 'Sendable'-conforming struct 'Box' has non-sendable type 
  let action: @MainActor () -> Void
}

Compiler bug? Or do I need to explicitly mark as Sendable also?

struct Box: Sendable {
  // Hunky-dory
  let action: @Sendable @MainActor () -> Void
}
1 Like

Here's another one I'm less sure of, but I'm not clear on why this wouldn't work:

@MainActor final class C {}
@MainActor protocol P: AnyObject {}

struct S {
  // Happy days
  static let defaultValueA: C? = nil
  // Static property 'defaultValueB' is not concurrency-safe because it is not either conforming to 'Sendable' or isolated to a global actor; this is an error in Swift 6
  static let defaultValueB: P? = nil
}

There is a new proposal just added on that topic: [Pitch] Usability of global-actor-isolated types. So yes, @MainActor isolated closures should imply sendability.

3 Likes

These checks are not very reliable yet. For example:

import Foundation

@MainActor func mainThreadCall() {}

func foo() {
    DispatchQueue.main.async(execute: mainThreadCall) // βœ… (expected)
    let queue = DispatchQueue.main
    queue.async(execute: mainThreadCall)
//                     πŸ”Άβ•°β”€ warning: converting function value of type '@MainActor () -> ()' to '@convention(block) () -> Void' loses global actor 'MainActor'; this is an error in the Swift 6 language mode
}

func baz() {
    struct DispatchQueue {
        static let main = Foundation.DispatchQueue.global()
    }
    DispatchQueue.main.async(execute: mainThreadCall) // βœ… but should be a warning
}

Tested on nightly.

Thanks, all. Good to hear there's some fixes on the way. It's quite tough, as a lot of the time the diagnostics are very useful, but other times I'm questioning whether I'm misunderstanding something or the compiler is throwing a false positive.

Does anyone have any thoughts on the below? Should this be legal?

I am not sure about that, but it seems it should be safe to declare a global variable in that way. It is isolated on the global actor, so it is sendable by definition. But can be wrong on that one.

OK, thanks! Let me flag it in the pitch thread.

2 Likes

I have also been running into similar warnings.

Here's a simplified version of one of my current classes. Before Xcode 15.3, I was not getting these warnings.

@MainActor
class UIHandler {
    private let transformer: Transformer
    private let viewFactory: ViewFactory

    nonisolated init(
        transformer: Transformer,
        viewFactory: ViewFactory
    ) {
        self.transformer = transformer // Main actor-isolated property 'transformer' can not be mutated from a non-isolated context; this is an error in Swift 6
        self.viewFactory = viewStateFactory // Main actor-isolated property 'viewFactory' can not be mutated from a non-isolated context; this is an error in Swift 6
    }
}

protocol Transformer {}
final class ViewFactory {}

This warning is confusing because it states these properties are actor isolated, but they are not, from my understanding, given they are immutable.

I did find that marking the dependencies as Sendable keeps the compiler happy. Is marking @MainActor dependencies as Sendable a good approach, or is this a compiler bug?

Warning is confusing, yet the case here is that they are crossing isolation boundary - from the nonisolated init to the main actor isolation. And only sendable types can safely cross isolation boundary, so Sendable fixes that.

The rest depends on the use case. If your class is going to be initialized on main actor, you can make init isolated as well, therefore there will be no crossing of isolation boundary. Otherwise, conforming to Sendable is a go right now.

There are improvements to this with SE-414 introducing region based isolation (see here) and follow-up to it allowing transferring objects: [Pitch] `transferring` isolation regions of parameter and result values

For reference, I blogged about why this works the way it does: How the Swift compiler knows that DispatchQueue.main implies @MainActor

tl;dr: it's a hardcoded syntax check in the compiler that looks for DispatchQueue.main.async (and some related APIs), in exactly that spelling.

This was also discussed in this earlier thread: How does swift annotate that DispatchQueue.main.async runs on @MainActor but DispatchQueue.background.async doesn't

1 Like

Might as well chime in to call out that those missing annotations on some of the queue APIs we fixed and they properly infer main actor now: [6.0][Concurrency] Apply `@MainActor` to main dispatch queue operations without considering `Sendable`. by hborla Β· Pull Request #72565 Β· apple/swift Β· GitHub

But yes, there's baked-in knowledge about main dispatch queue like this.

// That will be funβ„’ if/when we decide to allow changing the main actor's executor to an custom executor someday, but that'll be an adventure for another time :sweat_smile:

2 Likes