[SE-0470] Prohibit inference of isolated conformances with nonisolated witnesses

If all of the witnesses to a conformance are nonisolated, then infer that
conformance as nonisolated rather than global-actor-isolated. This is
only relevant when InferIsolatedConformances is enabled, and prevents
that inference to help maintain source compatibility.
This commit is contained in:
Doug Gregor
2025-05-04 22:20:25 -07:00
parent 751678d7fb
commit 2ec6fbec4d
5 changed files with 115 additions and 10 deletions

View File

@@ -7991,17 +7991,51 @@ ConformanceIsolationRequest::evaluate(Evaluator &evaluator, ProtocolConformance
if (getActorIsolation(rootNormal->getProtocol()).isActorIsolated())
return ActorIsolation::forNonisolated(false);
// If we are inferring isolated conformances and the conforming type is
// isolated to a global actor, use the conforming type's isolation.
// Isolation inference rules follow. If we aren't inferring isolated conformances,
// we're done.
if (!ctx.LangOpts.hasFeature(Feature::InferIsolatedConformances))
return ActorIsolation::forNonisolated(false);
auto nominal = dc->getSelfNominalTypeDecl();
if (ctx.LangOpts.hasFeature(Feature::InferIsolatedConformances) &&
nominal) {
auto nominalIsolation = getActorIsolation(nominal);
if (nominalIsolation.isGlobalActor())
return nominalIsolation;
if (!nominal) {
return ActorIsolation::forNonisolated(false);
}
return ActorIsolation::forNonisolated(false);
// If we are inferring isolated conformances and the conforming type is
// isolated to a global actor, we may use the conforming type's isolation.
auto nominalIsolation = getActorIsolation(nominal);
if (!nominalIsolation.isGlobalActor()) {
return ActorIsolation::forNonisolated(false);
}
// If all of the value witnesses are nonisolated, then we should not infer
// global actor isolation.
bool anyIsolatedWitness = false;
auto protocol = conformance->getProtocol();
for (auto requirement : protocol->getMembers()) {
if (isa<TypeDecl>(requirement))
continue;
auto valueReq = dyn_cast<ValueDecl>(requirement);
if (!valueReq)
continue;
auto witness = conformance->getWitnessDecl(valueReq);
if (!witness)
continue;
auto witnessIsolation = getActorIsolation(witness);
if (witnessIsolation.isActorIsolated()) {
anyIsolatedWitness = true;
break;
}
}
if (!anyIsolatedWitness) {
return ActorIsolation::forNonisolated(false);
}
return nominalIsolation;
}
namespace {

View File

@@ -46,8 +46,8 @@ func acceptSendablePMeta<T: Sendable & P>(_: T.Type) { }
func acceptSendableQMeta<T: Sendable & Q>(_: T.Type) { }
nonisolated func testConformancesFromNonisolated() {
let _: any P = CExplicitMainActor() // expected-error{{main actor-isolated conformance of 'CExplicitMainActor' to 'P' cannot be used in nonisolated context}}
let _: any P = CImplicitMainActor() // expected-error{{main actor-isolated conformance of 'CImplicitMainActor' to 'P' cannot be used in nonisolated context}}
let _: any P = CExplicitMainActor() // okay
let _: any P = CImplicitMainActor() // okay
let _: any P = CNonIsolated()
let _: any P = CImplicitMainActorNonisolatedConformance()

View File

@@ -32,6 +32,11 @@ extension CExplicit: Q {
func g() { }
}
@SomeGlobalActor
class CViaNonisolatedWitness: P {
nonisolated func f() { } // okay! conformance above is nonisolated via this witness
}
// expected-error@+3{{conformance of 'CNonIsolated' to protocol 'P' crosses into global actor 'SomeGlobalActor'-isolated code and can cause data races}}
// expected-note@+2{{turn data races into runtime errors with '@preconcurrency'}}
// expected-note@+1{{isolate this conformance to the global actor 'SomeGlobalActor' with '@SomeGlobalActor'}}{{33-33=@SomeGlobalActor }}
@@ -49,4 +54,6 @@ nonisolated func testConformancesFromNonisolated() {
// Okay, these are nonisolated conformances.
let _: any Q = CExplicit()
let _: any P = CViaNonisolatedWitness()
}

View File

@@ -2881,6 +2881,58 @@ public struct HangingMacro: PeerMacro {
}
}
public struct PWithNonisolatedFuncMacro: ExtensionMacro {
public static var inferNonisolatedConformances: Bool { false }
public static func expansion(
of node: AttributeSyntax,
attachedTo decl: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
if (protocols.isEmpty) {
return []
}
let decl: DeclSyntax =
"""
extension \(raw: type.trimmedDescription): P {
nonisolated static func requirement() { }
}
"""
return [
decl.cast(ExtensionDeclSyntax.self)
]
}
}
public struct NonisolatedPWithNonisolatedFuncMacro: ExtensionMacro {
public static func expansion(
of node: AttributeSyntax,
attachedTo decl: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
if (protocols.isEmpty) {
return []
}
let decl: DeclSyntax =
"""
extension \(raw: type.trimmedDescription): P {
nonisolated static func requirement() { }
}
"""
return [
decl.cast(ExtensionDeclSyntax.self)
]
}
}
public struct BigEndianAccessorMacro: AccessorMacro {
public static func expansion(
of node: AttributeSyntax,

View File

@@ -283,3 +283,15 @@ struct HasNestedType {
// extensions of nested types when the outer type has an
// attached macro that can add other nested types.
extension HasNestedType.Inner {}
@attached(extension, conformances: P, names: named(requirement))
macro AddPWithNonisolated() = #externalMacro(module: "MacroDefinition", type: "PWithNonisolatedFuncMacro")
@attached(extension, conformances: P, names: named(requirement))
macro AddNonisolatedPWithNonisolated() = #externalMacro(module: "MacroDefinition", type: "NonisolatedPWithNonisolatedFuncMacro")
@AddNonisolatedPWithNonisolated
struct MakeMeNonisolated { }
@AddPWithNonisolated
struct KeepMeIsolated { }