Hi all,
I've been testing isolated conformance inference in combination with other features, especially default main-actor isolation (which implies InferIsolatedConformances
) , and I've found a few places where we probably want to tweak the inference rules.
Conformance to Sendable
-inheriting protocols
If a protocol inherits Sendable
, the conformance to it cannot have actor isolation because that would admit data races when it is used with generic code:
protocol P: Sendable { }
@MainActor
class C: @MainActor P { } // error: main-actor isolated conformance to 'Sendable' protocol P
We should adjust the conformance isolation inference rules to not introduce an actor-isolated conformance to a Sendable
protocol like P
:
@MainActor
class C2: P { // currently infers @MainActor conformance to P
// change to infer nonisolated conformances to P
}
Default actor isolation andSendable
-inheriting protocols
In practice, actor-isolated types like C
and c@
above are relatively hard to conform to protocols that inherit from Sendable
, because most of the members of the actor-isolated type will also be actor-isolated. Consider a class similar to C
and C2
that doesn't specify its isolation, e.g.,
protocol Q: Sendable {
func f()
}
class D: Q {
func f() { }
}
With the default main actor isolation introduced in SE-0466, this code will produce an error due to the way in which @MainActor
is inferred:
D
is inferred to@MainActor
because it isn't specified asnonisolated
D.f
is inferred to@MainActor
because it is part ofD
- The conformance
D: Q
remainsnonisolated
becauseQ
isSendable
. - There is an error in the non-isolated conformance
D: Q
because the main-actor-isolatedD.f
cannot satisfy the requirement for a non-isolatedQ.f()
.
Conformance to a Sendable
-inheriting protocol is a strong indication that a type cannot be main-actor-isolated. Therefore, SE-0466 should suppress inference of @MainActor
on types like D
that declare a conformance to a Sendable
-inheriting protocol in their primary definition.
nonisolated
members satisfying requirements
Inference of isolated conformances is specified to only look at the type and the protocol. However, that means that it will change the meaning of code that intentionally provides nonisolated members to satisfy a protocol requirement, like this:
protocol Q {
func f()
}
@MainActor
class D: Q {
nonisolated func f() { ... }
}
func acceptQ<T: Q>(_: T.Type) { }
nonisolated func passD() {
acceptQ(D.self) // accepted today
// error under InferIsolatedConformances because D: Q is @MainActor
}
Without InferIsolatedConformances
, the conformance of D
to Q
is non-isolated. With InferIsolatedConformances
, the conformance D: Q
is isolated to the main actor, even though it doesn't have to be. The fix is to mark the conformance itself as nonisolated
when you use InferIsolatedConformances
.
Instead, we could look at the declarations that are used to satisfy the conformance requirements. If they are all nonisolated
, then the conformance is nonisolated
. Then, this code would continue to work the same way it always has. This idea came up in the review discussion, but we rejected it due to concerns over its impact on compile times and the expectation that the migration to nonisolated
would be straightforward.
I've found two issues with this. The first is macros: macros like Observable
introduce nonisolated
members to satisfy protocol requirements. The macros were written with the expectation that the conformance had to be non-isolated, and don't know whether the type they're extending is actor-isolated or not. With SE-0470 as it is, the macros themselves will need to be updated to emit nonisolated
on the conformance. This isn't automatically migratable because the code is generated by a macro, so users of the macro will be affected by this source compatibility issue but can't directly fix the problem.
The second issue is that the impact of having the wrong isolation inferred can be far from where the problem was introduced. The inferred @MainActor
isolation on the conformance of D
to Q
, above, won't be apparent until someone tries to use that conformance from non-isolated code. That could easily be in a different module or package.
Synthesized conformances (e.g., Equatable, Hashable, Codable)
For several common protocols, like Equatable
, Hashable
, and the two protocols that make up Codable
(Encodable
and Decodable
), the Swift compiler introduces members to satisfy the conformance requirements (e.g., ==
, hash(into:)
, and encode(to:)
). Similar to the issue with macros above, the compiler has always introduced these as nonisolated
members. Instead, the compiler should only make these nonisolated
when the type itself is not global-actor isolated. Otherwise, they should get their isolation inferred in the same way as other members do. In practice, this means that main-actor types can make use of synthesized, isolated conformances:
@MainActor
class C: Codable {
// ... okay, conformance is infferred @MainActor, as are init(from:) and encode(to:).
}
Cheers,
Doug