Help understand global-actor isolated conformance inference

I’m trying to understand how Swift 6 Concurrency works and even after reading SE-0470 and SE-0466 I can’t explain why one of the following declaration produces error and others don’t. Would be grateful for explanation or advice about what to read to understand concurrency better.

Swift 6 language mode with Approachable concurrency, default usolation to @MainActorMainActor, Complete Strict concurrency checking

protocol Message: Decodable {
    func process()
}

// This is the only error, which I think I understand - Decodable is not isolated, generated implementation is isolated
// Conformance of 'TestMessage' to protocol 'Message' crosses into main actor-isolated code and can cause data races
struct TestMessage: Message {
    func process() { }
}

struct TestMessage2: Decodable { // 1. Why @MainActor conformance inferred here but not for Message?
    func process() { }
}

nonisolated protocol MessageNonIsolated: Decodable {
    func process()
}

struct TestMessage3: MessageNonIsolated { // 2. Why @MainActor conformance inferred here but not for Message?
    func process() { }
}

struct TestMessage4: @MainActor Message {
    nonisolated func process() { } // 3. Isn't process should be @MainActor isolated?
}
1 Like

Bizarrely, if you make Message a protocol that doesn't refine Decodable, you get @MainActor inference again:

protocol Message {
    func process()
}

// ✅ (Inferred a '@MainActor Message' conformance)
struct TestMessage: Message {
    func process() { }
}

Not sure if it's intentional. I'd say it isn't.


nonisolated protocol MessageNonIsolated: Decodable {
    func process()
}

struct TestMessage3: MessageNonIsolated { // 2. Why @MainActor conformance inferred here but not for Message?
    func process() { }
}

Here the protocol is explicitly nonisolated. If the protocol defines an isolation, and you conform to the protocol in the type declaration (not an extension), the type will be inferred to have the same isolation as the protocol.

So the explicit nonisolated in the protocol is supressing the default @MainActor isolation.


struct TestMessage4: @MainActor Message {
    nonisolated func process() { } // 3. Isn't process should be @MainActor isolated?
}

You can conform to a @MainActor requirement with a nonisolated implementation. Even in cases like this one:

protocol P {
    @MainActor func foo()
}

struct S: P {
    nonisolated func foo() {} // ✅ Allowed
}

This is because nonisolated just means the function is safe to be called from any isolation, including the Main Actor. So, in a way, the implementation is more 'general' than the requirement.

This is similar to how you can write a non-throwing implementation to a requirement that throws:

protocol P {
    func foo() throws
}

struct S: P {
    func foo() {} // ✅ Also allowed
}
1 Like