[Review manager hat on.]
Keep in mind that this amendment includes two proposed inference changes. Even if you feel one of them is very intuitive and obviously useful, you may want to review both sets of changes carefully to ensure that they meet your needs.
[Review manager hat off.]
I think it's worth explicitly pointing out the source evolution trade-off involved in inferring non-isolated conformance when all declarations that satisfy protocol requirements are themselves non-isolated (whether by default or explicitly).
It's currently the case (with some corner case caveats) that a protocol can add a new requirement with a default implementation while maintaining source compatibility. With the currently proposed inference rule, we are adding a major caveat where that will not be the case.
Consider the following scenario:
// Library A v1.0 (nonisolated by default)
protocol Q {
static func create() -> Self
}
// MyApp (main actor by default)
class MyType: Q {
nonisolated static func create() -> MyType { ... }
func destroy() { ... }
}
With the proposed amendment as-is, the conformance of MyType to Q is inferred non-isolated.
If library A (v1.1) adds a new requirement Q.destroy() with a default implementation, the proposed amendment would suddenly infer @MainActor-isolated conformance to Q. This is because the existing MyType.destroy() would be a suitable @MainActor-isolated implementation of the new requirement. Since not all of Q's requirements are implemented by MyType in a non-isolated manner, the conformance won't be non-isolated. This change in the inferred isolation is likely to be source-breaking even if the author of library A intended no source-breaking changes and did not tag a semver major release.
For completeness, there are two alternatives I can think of, each with their pros and cons:
-
(Covered in the current alternatives considered.) Require explicit use of nonisolated instead of inferring it.
-
Consider only non-defaulted requirements in the non-isolated inference rule (provided that the default implementations are non-isolated).
Alternative (1) would produce the desired evolutionary behavior in a straightforward way. Diagnostics at the time of implementation will prompt the user to express the intended isolation of the conformance. Once explicitly stated in code, any future evolution of the protocol will lead to diagnostics that guide the user to adjust their implementation of an added requirement, adopt the default implementation, or change the isolation of the conformance.
One downside of this alternative is that users could be exposed to nonisolated as a concept before they're ready from a progressive disclosure standpoint. However, as far as I can tell, a user would have to explicitly annotate something as either @MainActor and/or nonisolated in order to trigger the scenario in question. An exception is in the case of macro-generated code, but that leads to a larger philosophical conversation I'd subset out from here.
Alternative (2) would produce the desired evolutionary behavior also. It would allow MyApp in this example to have the same behavior whether recompiled or not on an ABI-stable platform. That is, the nonisolated default implementation would remain the witness for the new protocol requirement and the conformance would continue to be inferred nonisolated.
However, from a compiler standpoint, I'd expect this alternative to require more implementation complexity. And though already unavoidable even at present on ABI-stable platforms if MyApp isn't recompiled, we'd also be foregrounding complexity in the user-facing model because Swift here will distinguish two implementations of destroy() based on their static isolation in determining whether one overrides or shadows the other.
This alternative could lead to surprising inferences upfront if a user conforms to a protocol P with all-defaulted implementations in @MainActor-isolated mode. Namely, if they have written concrete implementations for all of P's requirements but they are all @MainActor-isolated, they could unwittingly end up with a nonisolated conformance they won't ever need that ignores their custom implementations. We'd likely want to broaden conformance near-miss diagnostics to detect isolation mismatches for that reason, and those will still necessarily be imperfect.
To my mind, however, since default implementations are supposed to be appropriate for all conforming types, this may still be a superior trade-off in that the downsides of having an unexpectedly non-isolated and less-optimal (but correct) conformance are better than the downsides of an unexpectedly isolated (and thereby source-breaking) conformance.