Default Main Actor isolation and protocol inheritance

I’ve been trying to understand how library code may work with new application targets that use main actor isolation and Swift 6 language mode and came across the following behavior.

Given library code with a basic protocol hierarchy:

protocol P1 { func f() }
protocol P2: P1 {}

And an app using main actor isolation and Swift 6 language mode:

struct S1: P1 { func f() }
struct S2: P2 { func f() }

S1 compiles fine, while S2 fails with:

Conformance of 'S' to protocol 'P2' crosses into main actor-isolated code and can cause data races

I’ve uploaded a project that reproduces the behavior here.

The error can be worked around with an explicit @MainActor:

struct S2: @MainActor P2 { func f() }

But I would hope that would be inferred automatically, especially since an extension macro can apply a conformance but can’t know whether or not it should do so with a specific isolation.

A similar behavior can be demonstrated with a standard library protocol like Collection:

struct MyCollection: Collection {
  var startIndex: Int { 0 }
  var endIndex: Int { 0 }
  func index(after i: Int) -> Int { i + 1 }
  subscript(offset: Int) -> String { "" }
  // Comment the following in and it builds:
  // func makeIterator() -> AnyIterator<String> {
  //   fatalError()
  // }
}

Conformance of 'MyCollection' to protocol 'Sequence' crosses into main actor-isolated code and can cause data races

This can also be worked around with explicit @MainActor, but curiously it can also be worked around with a manual makeIterator implementation.

Is all of this expected behavior? Are they bugs that should be reported?

4 Likes

Opened an issue here: Default main actor isolation fails to deeply apply to some protocol hierarchies · Issue #82222 · swiftlang/swift · GitHub

/cc @hborla (I've seen you tagged in some other threads for default main actor conversation if it helps to flag. If it does not help let me know and I'll refrain in the future :smile:)

2 Likes

Thank you for the report! I agree that this is a compiler bug, and a main actor isolated conformance should be inferred through protocol refinement.

2 Likes

@stephencelis (cc: @hborla) Found a potentially related issue that isn't addressed with the @MainActor workaround.

Example:

struct S1 {
	let description: String
}

extension S1: LocalizedError {
	var errorDescription: String? {
		description
	}
}

That generates "Conformance of 'S1' to protocol 'LocalizedError' crosses into main actor-isolated code and can cause data races"

If prefixing the LocalizedError conformance with @MainActor, you then get a "Main actor-isolated conformance of 'S1' to 'Error' cannot satisfy conformance requirement for a 'Sendable' type parameter 'Self'" error.

In turn, then making S1 conform to Sendable generates the same error.

The only way I've found to remove the error is to prefix with @preconcurrency:

struct S1 {
	let description: String
}

extension S1: @preconcurrency LocalizedError {
	var errorDescription: String? {
		description
	}
}

That generates "Conformance of 'S1' to protocol 'LocalizedError' crosses into main actor-isolated code and can cause data races"

LocalizedError in this case conforms to Error, which is Sendable. So we want to allow the witness of LocalizedError, errorDescription to be called from any concurrency domain, which is why you can't conform to this protocol with a main-actor isolated description. One way to address this is to mark S1 with nonisolated to decouple it from the main actor.

That being said, it is not great that the suggested fix-it to mark the conformance with @MainActor is not helpful here.

I just tried marking the conforming type as nonisolated but that led to a cascading set of compile errors.

Interestingly, If I instead conform S1 to @preconcurrency Error, it gives a warning of "'@preconcurrency' on conformance to 'Error' has no effect"

And dropping @preconcurrency leads to no compile errors.

In this particular case, I only need to conform to Error, so can end up not having any workarounds in place. Especially happy to no longer have any @preconcurrency usages.

Anyone know when Apple will include this fix in Xcode? Since the PR for this has been merged to Swift 6.2 main, two Xcode beta releases have been issued (3 and 4). But the problem still persists, so my assumption is that it hasn't yet been folded into what is shipped with Xcode.