I'm glad you brought this up! I've been meaning to revisit some of the longstanding quirks of how explicit nonisolated modifiers behave. I believe this is due to a discrepancy with how explicit nonisolated works when written on protocol requirements that has always existed as far as I can tell. Here's an example, showing the Swift 6.0 behavior before we had isolated conformances:
// compiling with Swift 6.0 under '-language-mode 6'
protocol P {
nonisolated func f()
}
protocol Q {
func g()
}
@MainActor struct S1: P {
// Okay, 'f' is inferred to be 'nonisolated'
func f() {}
}
@MainActor struct S2: Q {
// Error, 'g' is inferred to be '@MainActor' and can't satisfy a 'nonisolated' requirement
func g() {}
}
With isolated conformances in Swift 6.2, both conformances compile, but S1 has a nonisolated conformance to P and S2 has a main actor isolated conformance to Q.
I find this behavior extremely surprising. Both protocol requirements are nonisolated, and whether or not you write it explicitly shouldn't be load bearing for how inference works on the conforming type.
Isolation inference for class overrides is not different between explicit vs implicit nonisolated, but it's still surprising:
class C1 {
nonisolated func f() {}
}
class C2 {
func g() {}
}
@MainActor class D1: C1 {
// Okay, 'f' is inferred to be 'nonisolated'
override func f() {}
}
@MainActor class D2: C2 {
// Okay, 'g' is inferred to be 'nonisolated'
override func g() {}
}
For class overrides, an explicit nonisolated does not make a difference, and nonisolated is always inferred on the overridden method if the superclass method is nonisolated. If we want to have the notion of isolated subclasses and overrides, this is not the inference rule that we would want.
The representation of actor isolation in the compiler does distinguish between "default isolation" and "nonisolated", which is how the compiler treats these cases differently. I have no idea how protocol refinement is somehow messing with whether the original requirement is default isolated or nonisolated, but my guess is there's some funny behavior that tries to cut off isolation inference in a case like this:
@MainActor protocol A {
func f()
}
nonisolated protocol B: A {}
struct S: B {
// infer 'f' as 'nonisolated'
func f()
}
My opinion is that we should make the semantics consistent regardless of whether nonisolated is explicit or inferred via the default isolation, and unify these cases in the implementation as well. In your example, I think func requirement() inside @MainActor final class MyClass should always have@MainActor inferred from the context, and never have nonisolated inferred from the protocol requirement it satisfies. This is actually implemented behind an experimental feature (NoExplicitNonisolated) that "just" needs some source compatibility testing to determine whether we really need an upcoming feature to fix these bugs! I do plan to write up a proposal for this (and would welcome collaborators!) because as you both mention, the rules are not written down anywhere and they're pretty nuanced.
This is just because isolation inference from protocol requirements only happens if the implementation is written in the same scope as the conformance.