Skip to content

[SE-0470] Enable isolated conformances by default #80810

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,34 @@
strategy, given that the code would eventually become ambiguous anyways when
the deployment target is raised.

* [SE-0470][]:
A protocol conformance can be isolated to a specific global actor, meaning that the conformance can only be used by code running on that actor. Isolated conformances are expressed by specifying the global actor on the conformance itself:

```swift
protocol P {
func f()
}

@MainActor
class MyType: @MainActor P {
/*@MainActor*/ func f() {
// must be called on the main actor
}
}
```

Swift will produce diagnostics if the conformance is directly accessed in code that isn't guaranteed to execute in the same global actor. For example:

```swift
func acceptP<T: P>(_ value: T) { }

/*nonisolated*/ func useIsolatedConformance(myType: MyType) {
acceptP(myType) // error: main actor-isolated conformance of 'MyType' to 'P' cannot be used in nonisolated context
}
```

To address such issues, only use an isolated conformance from code that executes on the same global actor.

* [SE-0419][]:
Introduced the new `Runtime` module, which contains a public API that can
generate backtraces, presently supported on macOS and Linux. Capturing a
Expand Down Expand Up @@ -10759,6 +10787,7 @@ using the `.dynamicType` member to retrieve the type of an expression should mig
[SE-0442]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0442-allow-taskgroup-childtaskresult-type-to-be-inferred.md
[SE-0444]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md
[SE-0458]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0458-strict-memory-safety.md
[SE-0470]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0470-isolated-conformances.md
[#64927]: <https://github.com/apple/swift/issues/64927>
[#42697]: <https://github.com/apple/swift/issues/42697>
[#42728]: <https://github.com/apple/swift/issues/42728>
Expand Down
1 change: 1 addition & 0 deletions include/swift/AST/ASTContextGlobalCache.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ struct WitnessIsolationError {
/// Describes an isolation error involving an associated conformance.
struct AssociatedConformanceIsolationError {
ProtocolConformance *isolatedConformance;
DiagnosticBehavior behavior = DiagnosticBehavior::Unspecified;

/// Diagnose this associated conformance isolation error.
void diagnose(const NormalProtocolConformance *conformance) const;
Expand Down
4 changes: 0 additions & 4 deletions include/swift/AST/DiagnosticsSema.def
Original file line number Diff line number Diff line change
Expand Up @@ -8486,10 +8486,6 @@ ERROR(attr_abi_failable_mismatch,none,
//===----------------------------------------------------------------------===//
// MARK: Isolated conformances
//===----------------------------------------------------------------------===//
GROUPED_ERROR(isolated_conformance_experimental_feature,IsolatedConformances,
none,
"isolated conformances require experimental feature "
" 'IsolatedConformances'", ())
NOTE(note_isolate_conformance_to_global_actor,none,
"isolate this conformance to the %select{global actor %0|main actor}1 "
"with '@%2'", (Type, bool, StringRef))
Expand Down
3 changes: 3 additions & 0 deletions include/swift/AST/ExistentialLayout.h
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ struct ExistentialLayout {
/// calling this on a temporary is likely to be incorrect.
ArrayRef<ProtocolDecl*> getProtocols() const && = delete;

/// Determine whether this refers to any non-marker protocols.
bool containsNonMarkerProtocols() const;

ArrayRef<ParameterizedProtocolType *> getParameterizedProtocols() const & {
return parameterized;
}
Expand Down
13 changes: 13 additions & 0 deletions include/swift/AST/GenericSignature.h
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,19 @@ class alignas(1 << TypeAlignInBits) GenericSignatureImpl final
/// the given protocol.
bool requiresProtocol(Type type, ProtocolDecl *proto) const;

/// Determine whether a conformance requirement of the given type to the
/// given protocol prohibits the use of an isolated conformance.
///
/// The use of an isolated conformance to satisfy a requirement T: P is
/// prohibited when T is a type parameter and T, or some type that can be
/// used to reach T, also conforms to Sendable or SendableMetatype. In that
/// case, the conforming type and the protocol (Sendable or SendableMetatype)
/// is returned.
///
/// If there is no such requirement, returns std::nullopt.
std::optional<std::pair<Type, ProtocolDecl *>>
prohibitsIsolatedConformance(Type type) const;

/// Determine whether the given dependent type is equal to a concrete type.
bool isConcreteType(Type type) const;

Expand Down
8 changes: 2 additions & 6 deletions include/swift/Basic/Features.def
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ SUPPRESSIBLE_LANGUAGE_FEATURE(MemorySafetyAttributes, 458, "@unsafe attribute")
LANGUAGE_FEATURE(ValueGenerics, 452, "Value generics feature (integer generics)")
LANGUAGE_FEATURE(RawIdentifiers, 451, "Raw identifiers")
LANGUAGE_FEATURE(SendableCompletionHandlers, 463, "Objective-C completion handler parameters are imported as @Sendable")
LANGUAGE_FEATURE(IsolatedConformances, 470, "Global-actor isolated conformances")

// Swift 6
UPCOMING_FEATURE(ConciseMagicFile, 274, 6)
Expand All @@ -276,6 +277,7 @@ UPCOMING_FEATURE(GlobalActorIsolatedTypesUsability, 0434, 6)
ADOPTABLE_UPCOMING_FEATURE(ExistentialAny, 335, 7)
UPCOMING_FEATURE(InternalImportsByDefault, 409, 7)
UPCOMING_FEATURE(MemberImportVisibility, 444, 7)
UPCOMING_FEATURE(InferIsolatedConformances, 470, 7)

// Optional language features / modes

Expand Down Expand Up @@ -500,12 +502,6 @@ EXPERIMENTAL_FEATURE(CustomAvailability, true)
/// is resilient or not.
EXPERIMENTAL_FEATURE(ExtensibleEnums, true)

/// Allow isolated conformances.
EXPERIMENTAL_FEATURE(IsolatedConformances, true)

/// Infer conformance isolation on global-actor-conforming types.
EXPERIMENTAL_FEATURE(InferIsolatedConformances, true)

/// Allow SwiftSettings
EXPERIMENTAL_FEATURE(SwiftSettings, false)

Expand Down
83 changes: 59 additions & 24 deletions lib/AST/ConformanceLookup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ swift::collectExistentialConformances(CanType fromType,
return fromType->getASTContext().AllocateCopy(conformances);
}

static bool containsNonMarkerProtocols(ArrayRef<ProtocolDecl *> protocols) {
for (auto proto : protocols) {
if (!proto->isMarkerProtocol())
return true;
}

return false;
}

ProtocolConformanceRef
swift::lookupExistentialConformance(Type type, ProtocolDecl *protocol) {
ASTContext &ctx = protocol->getASTContext();
Expand Down Expand Up @@ -146,6 +155,12 @@ swift::lookupExistentialConformance(Type type, ProtocolDecl *protocol) {
if (auto conformance = lookupSuperclassConformance(layout.getSuperclass()))
return conformance;

// If the protocol is SendableMetatype, and there are no non-marker protocol
// requirements, allow it via self-conformance.
if (protocol->isSpecificProtocol(KnownProtocolKind::SendableMetatype) &&
!layout.containsNonMarkerProtocols())
return ProtocolConformanceRef(ctx.getSelfConformance(protocol));

// We didn't find our protocol in the existential's list; it doesn't
// conform.
return ProtocolConformanceRef::forInvalid();
Expand Down Expand Up @@ -377,6 +392,48 @@ static ProtocolConformanceRef getBuiltinFunctionTypeConformance(
return ProtocolConformanceRef::forMissingOrInvalid(type, protocol);
}

/// Given the instance type of a metatype, determine whether the metatype is
/// Sendable.
///
// Metatypes are generally Sendable, but with isolated conformances we
// cannot assume that metatypes based on type parameters are Sendable.
// Therefore, check for conformance to SendableMetatype.
static bool metatypeWithInstanceTypeIsSendable(Type instanceType) {
ASTContext &ctx = instanceType->getASTContext();

// If we don't have the SendableMetatype protocol at all, just assume all
// metatypes are Sendable.
auto sendableMetatypeProto =
ctx.getProtocol(KnownProtocolKind::SendableMetatype);
if (!sendableMetatypeProto)
return true;

// If the instance type is a type parameter, it is not necessarily
// SendableMetatype. There will need to be a SendableMetatype requirement,
// but we do not have the generic environment to check that.
if (instanceType->isTypeParameter())
return false;

// If the instance type conforms to SendableMetatype, then its
// metatype is Sendable.
auto instanceConformance = lookupConformance(
instanceType, sendableMetatypeProto);
if (!instanceConformance.isInvalid() &&
!instanceConformance.hasMissingConformance())
return true;

// If this is an archetype that is non-SendableMetatype, but there are no
// non-marker protocol requirements that could carry conformances, treat
// the metatype as Sendable.
if (auto archetype = instanceType->getAs<ArchetypeType>()) {
if (!containsNonMarkerProtocols(archetype->getConformsTo()))
return true;
}

// The instance type is non-Sendable.
return false;
}

/// Synthesize a builtin metatype type conformance to the given protocol, if
/// appropriate.
static ProtocolConformanceRef getBuiltinMetaTypeTypeConformance(
Expand All @@ -387,31 +444,9 @@ static ProtocolConformanceRef getBuiltinMetaTypeTypeConformance(
if (auto kp = protocol->getKnownProtocolKind()) {
switch (*kp) {
case KnownProtocolKind::Sendable:
// Metatypes are generally Sendable, but with isolated conformances we
// cannot assume that metatypes based on type parameters are Sendable.
// Therefore, check for conformance to SendableMetatype.
if (ctx.LangOpts.hasFeature(Feature::IsolatedConformances)) {
auto sendableMetatypeProto =
ctx.getProtocol(KnownProtocolKind::SendableMetatype);
if (sendableMetatypeProto) {
Type instanceType = metatypeType->getInstanceType();

// If the instance type is a type parameter, it is not necessarily
// Sendable. There will need to be a Sendable requirement.
if (instanceType->isTypeParameter())
break;

// If the instance type conforms to SendableMetatype, then its
// metatype is Sendable.
auto instanceConformance = lookupConformance(
instanceType, sendableMetatypeProto);
if (instanceConformance.isInvalid() ||
instanceConformance.hasMissingConformance())
break;
}
if (!metatypeWithInstanceTypeIsSendable(metatypeType->getInstanceType()))
break;

// Every other metatype is Sendable.
}
LLVM_FALLTHROUGH;

case KnownProtocolKind::Copyable:
Expand Down
38 changes: 38 additions & 0 deletions lib/AST/GenericSignature.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,44 @@ bool GenericSignatureImpl::requiresProtocol(Type type,
return getRequirementMachine()->requiresProtocol(type, proto);
}

std::optional<std::pair<Type, ProtocolDecl *>>
GenericSignatureImpl::prohibitsIsolatedConformance(Type type) const {
type = getReducedType(type);

if (!type->isTypeParameter())
return std::nullopt;

// An isolated conformance cannot be used in a context where the type
// parameter can escape the isolation domain in which the conformance
// was formed. To establish this, we look for Sendable or SendableMetatype
// requirements on the type parameter itself.
ASTContext &ctx = type->getASTContext();
auto sendableProto = ctx.getProtocol(KnownProtocolKind::Sendable);
auto sendableMetatypeProto =
ctx.getProtocol(KnownProtocolKind::SendableMetatype);

// Check for a conformance requirement to SendableMetatype, which is
// implied by Sendable.
if (sendableMetatypeProto && requiresProtocol(type, sendableMetatypeProto)) {
// Check for a conformance requirement to Sendable and return that if
// it exists, because it's more recognizable and specific.
if (sendableProto && requiresProtocol(type, sendableProto))
return std::make_pair(type, sendableProto);

return std::make_pair(type, sendableMetatypeProto);
}

// If this is a nested type, also check whether the parent type conforms to
// SendableMetatype, because one can derive this type from the parent type.
// FIXME: This is not a complete check, because there are other ways in which
// one might be able to derive this type. This needs to determine whether
// there is any path from a SendableMetatype-conforming type to this type.
if (auto depMemTy = type->getAs<DependentMemberType>())
return prohibitsIsolatedConformance(depMemTy->getBase());

return std::nullopt;
}

/// Determine whether the given dependent type is equal to a concrete type.
bool GenericSignatureImpl::isConcreteType(Type type) const {
assert(type->isTypeParameter() && "Expected a type parameter");
Expand Down
47 changes: 38 additions & 9 deletions lib/AST/Type.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,15 @@ bool ExistentialLayout::isExistentialWithError(ASTContext &ctx) const {
return false;
}

bool ExistentialLayout::containsNonMarkerProtocols() const {
for (auto proto : getProtocols()) {
if (!proto->isMarkerProtocol())
return true;
}

return false;
}

LayoutConstraint ExistentialLayout::getLayoutConstraint() const {
if (hasExplicitAnyObject) {
return LayoutConstraint::getLayoutConstraint(
Expand Down Expand Up @@ -5006,11 +5015,15 @@ StringRef swift::getNameForParamSpecifier(ParamSpecifier specifier) {
llvm_unreachable("bad ParamSpecifier");
}

std::optional<DiagnosticBehavior>
TypeBase::getConcurrencyDiagnosticBehaviorLimit(DeclContext *declCtx) const {
auto *self = const_cast<TypeBase *>(this);
static std::optional<DiagnosticBehavior>
getConcurrencyDiagnosticBehaviorLimitRec(
Type type, DeclContext *declCtx,
llvm::SmallPtrSetImpl<NominalTypeDecl *> &visited) {
if (auto *nomDecl = type->getNominalOrBoundGenericNominal()) {
// If we have already seen this type, treat it as having no limit.
if (!visited.insert(nomDecl).second)
return std::nullopt;

if (auto *nomDecl = self->getNominalOrBoundGenericNominal()) {
// First try to just grab the exact concurrency diagnostic behavior.
if (auto result =
swift::getConcurrencyDiagnosticBehaviorLimit(nomDecl, declCtx)) {
Expand All @@ -5021,11 +5034,12 @@ TypeBase::getConcurrencyDiagnosticBehaviorLimit(DeclContext *declCtx) const {
// merging our fields if we have a struct.
if (auto *structDecl = dyn_cast<StructDecl>(nomDecl)) {
std::optional<DiagnosticBehavior> diagnosticBehavior;
auto substMap = self->getContextSubstitutionMap();
auto substMap = type->getContextSubstitutionMap();
for (auto storedProperty : structDecl->getStoredProperties()) {
auto lhs = diagnosticBehavior.value_or(DiagnosticBehavior::Unspecified);
auto astType = storedProperty->getInterfaceType().subst(substMap);
auto rhs = astType->getConcurrencyDiagnosticBehaviorLimit(declCtx);
auto rhs = getConcurrencyDiagnosticBehaviorLimitRec(astType, declCtx,
visited);
auto result = lhs.merge(rhs.value_or(DiagnosticBehavior::Unspecified));
if (result != DiagnosticBehavior::Unspecified)
diagnosticBehavior = result;
Expand All @@ -5036,21 +5050,36 @@ TypeBase::getConcurrencyDiagnosticBehaviorLimit(DeclContext *declCtx) const {

// When attempting to determine the diagnostic behavior limit of a tuple, just
// merge for each of the elements.
if (auto *tupleType = self->getAs<TupleType>()) {
if (auto *tupleType = type->getAs<TupleType>()) {
std::optional<DiagnosticBehavior> diagnosticBehavior;
for (auto tupleType : tupleType->getElements()) {
auto lhs = diagnosticBehavior.value_or(DiagnosticBehavior::Unspecified);

auto type = tupleType.getType()->getCanonicalType();
auto rhs = type->getConcurrencyDiagnosticBehaviorLimit(declCtx);
auto rhs = getConcurrencyDiagnosticBehaviorLimitRec(type, declCtx,
visited);
auto result = lhs.merge(rhs.value_or(DiagnosticBehavior::Unspecified));
if (result != DiagnosticBehavior::Unspecified)
diagnosticBehavior = result;
}
return diagnosticBehavior;
}

return {};
// Metatypes that aren't Sendable were introduced in Swift 6.2, so downgrade
// them to warnings prior to Swift 7.
if (type->is<AnyMetatypeType>()) {
if (!type->getASTContext().LangOpts.isSwiftVersionAtLeast(7))
return DiagnosticBehavior::Warning;
}

return std::nullopt;
}

std::optional<DiagnosticBehavior>
TypeBase::getConcurrencyDiagnosticBehaviorLimit(DeclContext *declCtx) const {
auto *self = const_cast<TypeBase *>(this);
llvm::SmallPtrSet<NominalTypeDecl *, 16> visited;
return getConcurrencyDiagnosticBehaviorLimitRec(Type(self), declCtx, visited);
}

GenericTypeParamKind
Expand Down
4 changes: 1 addition & 3 deletions lib/Parse/ParseType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,7 @@ ParserResult<TypeRepr> Parser::parseTypeSimple(
Diag<> MessageID, ParseTypeReason reason) {
ParserResult<TypeRepr> ty;

if (isParameterSpecifier() &&
!(!Context.LangOpts.hasFeature(Feature::IsolatedConformances) &&
Tok.isContextualKeyword("isolated"))) {
if (isParameterSpecifier()) {
// Type specifier should already be parsed before here. This only happens
// for construct like 'P1 & inout P2'.
diagnose(Tok.getLoc(), diag::attr_only_on_parameters, Tok.getRawText());
Expand Down
Loading