Diagnose captured of non-sendable metatypes crossing isolation boundaries

Keep track of all of the type parameters and archetypes that are captured
by a local function or closure. Use that information to diagnose cases
where a non-Sendable metatype crosses an isolation boundary.
This commit is contained in:
Doug Gregor
2025-02-13 14:36:52 -08:00
parent b1e9673a47
commit ed6dccf12c
10 changed files with 210 additions and 84 deletions

View File

@@ -17,6 +17,7 @@
#include "swift/Basic/LLVM.h"
#include "swift/Basic/OptionSet.h"
#include "swift/Basic/SourceLoc.h"
#include "swift/AST/Type.h"
#include "swift/AST/TypeAlignments.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/PointerIntPair.h"
@@ -129,6 +130,18 @@ public:
unsigned getFlags() const { return Value.getInt(); }
};
/// Describes a type that has been captured by a closure or local function.
class CapturedType {
Type type;
SourceLoc loc;
public:
CapturedType(Type type, SourceLoc loc) : type(type), loc(loc) { }
Type getType() const { return type; }
SourceLoc getLoc() const { return loc; }
};
} // end swift namespace
namespace swift {
@@ -140,26 +153,32 @@ class CaptureInfo {
class CaptureInfoStorage final
: public llvm::TrailingObjects<CaptureInfoStorage,
CapturedValue,
GenericEnvironment *> {
GenericEnvironment *,
CapturedType> {
DynamicSelfType *DynamicSelf;
OpaqueValueExpr *OpaqueValue;
unsigned NumCapturedValues;
unsigned NumGenericEnvironments;
unsigned NumCapturedTypes;
public:
explicit CaptureInfoStorage(DynamicSelfType *dynamicSelf,
OpaqueValueExpr *opaqueValue,
unsigned numCapturedValues,
unsigned numGenericEnvironments)
unsigned numGenericEnvironments,
unsigned numCapturedTypes)
: DynamicSelf(dynamicSelf), OpaqueValue(opaqueValue),
NumCapturedValues(numCapturedValues),
NumGenericEnvironments(numGenericEnvironments) { }
NumGenericEnvironments(numGenericEnvironments),
NumCapturedTypes(numCapturedTypes) { }
ArrayRef<CapturedValue> getCaptures() const;
ArrayRef<GenericEnvironment *> getGenericEnvironments() const;
ArrayRef<CapturedType> getCapturedTypes() const;
DynamicSelfType *getDynamicSelfType() const {
return DynamicSelf;
}
@@ -171,6 +190,14 @@ class CaptureInfo {
unsigned numTrailingObjects(OverloadToken<CapturedValue>) const {
return NumCapturedValues;
}
unsigned numTrailingObjects(OverloadToken<GenericEnvironment *>) const {
return NumGenericEnvironments;
}
unsigned numTrailingObjects(OverloadToken<CapturedType>) const {
return NumCapturedTypes;
}
};
enum class Flags : unsigned {
@@ -187,7 +214,8 @@ public:
ArrayRef<CapturedValue> captures,
DynamicSelfType *dynamicSelf, OpaqueValueExpr *opaqueValue,
bool genericParamCaptures,
ArrayRef<GenericEnvironment *> genericEnv=ArrayRef<GenericEnvironment*>());
ArrayRef<GenericEnvironment *> genericEnv=ArrayRef<GenericEnvironment*>(),
ArrayRef<CapturedType> capturedTypes = ArrayRef<CapturedType>());
/// A CaptureInfo representing no captures at all.
static CaptureInfo empty();
@@ -196,11 +224,7 @@ public:
return StorageAndFlags.getPointer();
}
bool isTrivial() const {
assert(hasBeenComputed());
return getCaptures().empty() && !hasGenericParamCaptures() &&
!hasDynamicSelfCapture() && !hasOpaqueValueCapture();
}
bool isTrivial() const;
/// Returns all captured values and opaque expressions.
ArrayRef<CapturedValue> getCaptures() const {
@@ -214,6 +238,12 @@ public:
return StorageAndFlags.getPointer()->getGenericEnvironments();
}
/// Returns all captured values and opaque expressions.
ArrayRef<CapturedType> getCapturedTypes() const {
assert(hasBeenComputed());
return StorageAndFlags.getPointer()->getCapturedTypes();
}
/// \returns true if the function captures the primary generic environment
/// from its innermost declaration context.
bool hasGenericParamCaptures() const {

View File

@@ -5607,6 +5607,10 @@ ERROR(non_sendable_isolated_capture,none,
"capture of %1 with non-sendable type %0 in an isolated "
"%select{local function|closure}2",
(Type, DeclName, bool))
ERROR(non_sendable_metatype_capture,none,
"capture of non-sendable type %0 in an isolated "
"%select{local function|closure}1",
(Type, bool))
ERROR(self_capture_deinit_task,none,
"capture of 'self' in a closure that outlives deinit",
())

View File

@@ -54,6 +54,11 @@ CaptureInfo::CaptureInfoStorage::getGenericEnvironments() const {
return llvm::ArrayRef(this->getTrailingObjects<GenericEnvironment *>(), NumGenericEnvironments);
}
ArrayRef<CapturedType>
CaptureInfo::CaptureInfoStorage::getCapturedTypes() const {
return llvm::ArrayRef(this->getTrailingObjects<CapturedType>(), NumCapturedTypes);
}
//===----------------------------------------------------------------------===//
// MARK: CaptureInfo
//===----------------------------------------------------------------------===//
@@ -62,7 +67,8 @@ CaptureInfo::CaptureInfo(ASTContext &ctx, ArrayRef<CapturedValue> captures,
DynamicSelfType *dynamicSelf,
OpaqueValueExpr *opaqueValue,
bool genericParamCaptures,
ArrayRef<GenericEnvironment *> genericEnv) {
ArrayRef<GenericEnvironment *> genericEnv,
ArrayRef<CapturedType> capturedTypes) {
static_assert(IsTriviallyDestructible<CapturedValue>::value,
"Capture info is alloc'd on the ASTContext and not destroyed");
static_assert(IsTriviallyDestructible<CaptureInfo::CaptureInfoStorage>::value,
@@ -79,7 +85,8 @@ CaptureInfo::CaptureInfo(ASTContext &ctx, ArrayRef<CapturedValue> captures,
if (genericParamCaptures)
flags |= Flags::HasGenericParamCaptures;
if (captures.empty() && genericEnv.empty() && !dynamicSelf && !opaqueValue) {
if (captures.empty() && genericEnv.empty() && !dynamicSelf && !opaqueValue &&
capturedTypes.empty()) {
*this = CaptureInfo::empty();
StorageAndFlags.setInt(flags);
return;
@@ -87,29 +94,40 @@ CaptureInfo::CaptureInfo(ASTContext &ctx, ArrayRef<CapturedValue> captures,
size_t storageToAlloc =
CaptureInfoStorage::totalSizeToAlloc<CapturedValue,
GenericEnvironment *>(captures.size(),
genericEnv.size());
GenericEnvironment *,
CapturedType>(captures.size(),
genericEnv.size(),
capturedTypes.size());
void *storageBuf = ctx.Allocate(storageToAlloc, alignof(CaptureInfoStorage));
auto *storage = new (storageBuf) CaptureInfoStorage(dynamicSelf,
opaqueValue,
captures.size(),
genericEnv.size());
genericEnv.size(),
capturedTypes.size());
StorageAndFlags.setPointerAndInt(storage, flags);
std::uninitialized_copy(captures.begin(), captures.end(),
storage->getTrailingObjects<CapturedValue>());
std::uninitialized_copy(genericEnv.begin(), genericEnv.end(),
storage->getTrailingObjects<GenericEnvironment *>());
std::uninitialized_copy(capturedTypes.begin(), capturedTypes.end(),
storage->getTrailingObjects<CapturedType>());
}
CaptureInfo CaptureInfo::empty() {
static const CaptureInfoStorage empty{/*dynamicSelf*/nullptr,
/*opaqueValue*/nullptr,
0, 0};
0, 0, 0};
CaptureInfo result;
result.StorageAndFlags.setPointer(&empty);
return result;
}
bool CaptureInfo::isTrivial() const {
assert(hasBeenComputed());
return getCaptures().empty() && !hasGenericParamCaptures() &&
!hasDynamicSelfCapture() && !hasOpaqueValueCapture();
}
VarDecl *CaptureInfo::getIsolatedParamCapture() const {
for (const auto &capture : getCaptures()) {
// NOTE: isLocalCapture() returns false if we have dynamic self metadata
@@ -178,6 +196,12 @@ void CaptureInfo::print(raw_ostream &OS) const {
OS << genericEnv->getOpenedElementShapeClass();
},
[&] { OS << ","; });
interleave(getCapturedTypes(),
[&](CapturedType type) {
OS << " type=" << type.getType().getString();
},
[&] { OS << ","; });
OS << ')';
}

View File

@@ -4425,6 +4425,10 @@ TypeConverter::getLoweredLocalCaptures(SILDeclRef fn) {
// Captured pack element environments.
llvm::SetVector<GenericEnvironment *> genericEnv;
// Captured types.
SmallVector<CapturedType, 4> capturedTypes;
llvm::SmallDenseSet<CanType, 4> alreadyCapturedTypes;
bool capturesGenericParams = false;
DynamicSelfType *capturesDynamicSelf = nullptr;
OpaqueValueExpr *capturesOpaqueValue = nullptr;
@@ -4620,6 +4624,13 @@ TypeConverter::getLoweredLocalCaptures(SILDeclRef fn) {
// Collect non-function captures.
recordCapture(capture);
}
for (const auto &capturedType : captureInfo.getCapturedTypes()) {
if (alreadyCapturedTypes.insert(capturedType.getType()->getCanonicalType())
.second) {
capturedTypes.push_back(capturedType);
}
}
};
collectFunctionCaptures = [&](AnyFunctionRef curFn) {
@@ -4697,7 +4708,8 @@ TypeConverter::getLoweredLocalCaptures(SILDeclRef fn) {
// Cache the result.
CaptureInfo info(Context, resultingCaptures,
capturesDynamicSelf, capturesOpaqueValue,
capturesGenericParams, genericEnv.getArrayRef());
capturesGenericParams, genericEnv.getArrayRef(),
capturedTypes);
auto inserted = LoweredCaptures.insert({fn, info});
assert(inserted.second && "already in map?!");
(void)inserted;

View File

@@ -54,6 +54,10 @@ class FindCapturedVars : public ASTWalker {
/// can go here too.
llvm::SetVector<GenericEnvironment *> CapturedEnvironments;
/// The captured types.
SmallVector<CapturedType, 4> CapturedTypes;
llvm::SmallDenseMap<CanType, unsigned, 4> CapturedTypeEntryNumber;
SourceLoc GenericParamCaptureLoc;
SourceLoc DynamicSelfCaptureLoc;
DynamicSelfType *DynamicSelf = nullptr;
@@ -83,7 +87,8 @@ public:
return CaptureInfo(Context, Captures, dynamicSelfToRecord,
OpaqueValue, HasGenericParamCaptures,
CapturedEnvironments.getArrayRef());
CapturedEnvironments.getArrayRef(),
CapturedTypes);
}
bool hasGenericParamCaptures() const {
@@ -156,6 +161,22 @@ public:
}));
}
// Note that we're using a generic type.
auto recordUseOfGenericType = [&](Type type) {
if (!HasGenericParamCaptures) {
GenericParamCaptureLoc = loc;
HasGenericParamCaptures = true;
}
auto [insertionPos, inserted] = CapturedTypeEntryNumber.insert(
{type->getCanonicalType(), CapturedTypes.size()});
if (inserted) {
CapturedTypes.push_back(CapturedType(type, loc));
} else if (CapturedTypes[insertionPos->second].getLoc().isInvalid()) {
CapturedTypes[insertionPos->second] = CapturedType(type, loc);
}
};
// Similar to dynamic 'Self', IRGen doesn't really need type metadata
// for class-bound archetypes in nearly as many cases as with opaque
// archetypes.
@@ -174,23 +195,18 @@ public:
CapturedEnvironments.insert(env);
}
if ((t->is<PrimaryArchetypeType>() ||
t->is<PackArchetypeType>() ||
t->is<GenericTypeParamType>()) &&
!HasGenericParamCaptures) {
GenericParamCaptureLoc = loc;
HasGenericParamCaptures = true;
if (t->is<PrimaryArchetypeType>() ||
t->is<PackArchetypeType>() ||
t->is<GenericTypeParamType>()) {
recordUseOfGenericType(t);
}
}));
}
if (auto *gft = type->getAs<GenericFunctionType>()) {
TypeCaptureWalker walker(ObjC, [&](Type t) {
if (t->is<GenericTypeParamType>() &&
!HasGenericParamCaptures) {
GenericParamCaptureLoc = loc;
HasGenericParamCaptures = true;
}
if (t->is<GenericTypeParamType>())
recordUseOfGenericType(t);
});
for (const auto &param : gft->getParams())

View File

@@ -2777,6 +2777,17 @@ namespace {
/// Check closure captures for Sendable violations.
void checkLocalCaptures(AnyFunctionRef localFunc) {
auto *dc = getDeclContext();
ASTContext &ctx = dc->getASTContext();
auto *closure = localFunc.getAbstractClosureExpr();
auto *explicitClosure = dyn_cast_or_null<ClosureExpr>(closure);
bool preconcurrency = false;
if (explicitClosure) {
preconcurrency = explicitClosure->isIsolatedByPreconcurrency();
}
for (const auto &capture : localFunc.getCaptureInfo().getCaptures()) {
if (!capture.isLocalCapture())
continue;
@@ -2785,18 +2796,9 @@ namespace {
if (capture.isOpaqueValue())
continue;
auto *closure = localFunc.getAbstractClosureExpr();
auto *explicitClosure = dyn_cast_or_null<ClosureExpr>(closure);
bool preconcurrency = false;
if (explicitClosure) {
preconcurrency = explicitClosure->isIsolatedByPreconcurrency();
}
// Diagnose a `self` capture inside an escaping `sending`
// `@Sendable` closure in a deinit, which almost certainly
// means `self` would escape deinit at runtime.
auto *dc = getDeclContext();
if (explicitClosure && isa<DestructorDecl>(dc) &&
!explicitClosure->getType()->isNoEscape() &&
(explicitClosure->isPassedToSendingParameter() ||
@@ -2868,6 +2870,49 @@ namespace {
/*closure=*/closure != nullptr);
}
}
// FIXME: When passing to a sending parameter, should this be handled
// by region isolation? Or should it always be handled by region
// isolation?
if (ctx.LangOpts.hasFeature(Feature::StrictSendableMetatypes) &&
(mayExecuteConcurrentlyWith(
localFunc.getAsDeclContext(), getDeclContext()) ||
(explicitClosure && explicitClosure->isPassedToSendingParameter()))) {
GenericSignature genericSig;
if (auto afd = localFunc.getAbstractFunctionDecl())
genericSig = afd->getGenericSignature();
for (const auto &capturedType :
localFunc.getCaptureInfo().getCapturedTypes()) {
unsigned genericDepth;
Type type = capturedType.getType();
if (auto archetype = type->getAs<ArchetypeType>()) {
genericDepth = archetype->getInterfaceType()->getRootGenericParam()
->getDepth();
} else if (type->isTypeParameter()) {
genericDepth = type->getRootGenericParam()->getDepth();
type = localFunc.getAsDeclContext()->mapTypeIntoContext(type);
} else {
continue;
}
// If the local function is generic and this is one of its generic
// parameters, ignore it.
if (genericSig.getNextDepth() > 0 &&
genericDepth < genericSig.getNextDepth() - 1)
continue;
// Check that the metatype is sendable.
SendableCheckContext sendableContext(getDeclContext(), preconcurrency);
diagnoseNonSendableTypes(MetatypeType::get(type),
sendableContext,
/*inDerivedConformance*/Type(),
capturedType.getLoc(),
diag::non_sendable_metatype_capture,
/*closure=*/closure != nullptr);
}
}
}
public:

View File

@@ -16,20 +16,6 @@ func acceptMetaOnMainActor<T>(_: T.Type) { }
// -------------------------------------------------------------------------
// Non-Sendable metatype instances that cross into other isolation domains.
// -------------------------------------------------------------------------
nonisolated func staticCallThroughMeta<T: Q>(_: T.Type) {
let x = T.self
Task.detached { // expected-error{{risks causing data races}}
x.g() // expected-note{{closure captures 'x' which is accessible to code in the current task}}
}
}
nonisolated func passMeta<T: Q>(_: T.Type) {
let x = T.self
Task.detached { // expected-error{{risks causing data races}}
acceptMeta(x) // expected-note{{closure captures 'x' which is accessible to code in the current task}}
}
}
nonisolated func staticCallThroughMetaSmuggled<T: Q>(_: T.Type) {
let x: Q.Type = T.self
Task.detached { // expected-error{{risks causing data races}}
@@ -60,14 +46,6 @@ nonisolated func passToMainActorSmuggledAny<T: Q>(_: T.Type) async {
// -------------------------------------------------------------------------
// Sendable metatype instances that cross into other isolation domains.
// -------------------------------------------------------------------------
nonisolated func passMetaWithSendable<T: Sendable & Q>(_: T.Type) {
let x = T.self
Task.detached {
acceptMeta(x) // okay, because T is Sendable implies T.Type: Sendable
x.g() // okay, because T is Sendable implies T.Type: Sendable
}
}
nonisolated func passMetaWithSendableSmuggled<T: Sendable & Q>(_: T.Type) {
let x: any (Q & Sendable).Type = T.self
Task.detached {
@@ -80,21 +58,3 @@ nonisolated func passSendableToMainActorSmuggledAny<T: Sendable>(_: T.Type) asyn
let x: Sendable.Type = T.self
await acceptMetaOnMainActor(x)
}
// -------------------------------------------------------------------------
// Sendable requirements on metatypes
// -------------------------------------------------------------------------
nonisolated func passMetaWithMetaSendable<T: Q>(_: T.Type) where T.Type: Sendable {
// FIXME: Bogus errors below because we don't currently handle constraints on
// metatypes.
let x = T.self
Task.detached { // expected-error{{risks causing data races}}
acceptMeta(x) // expected-note{{closure captures 'x' which is accessible to code in the current task}}
// should be okay, because T is Sendable implies T.Type: Sendable
x.g() // okay, because T is Sendable implies T.Type: Sendable
}
}
// TODO: Cannot currently express an existential or opaque type where the
// metatype is Sendable.

View File

@@ -12,14 +12,49 @@ protocol Q {
nonisolated func acceptMeta<T>(_: T.Type) { }
nonisolated func staticCallThroughMetaVal<T: Q>(_: T.Type) {
let x = T.self // expected-error{{capture of non-sendable type 'T.Type' in an isolated closure}}
Task.detached {
x.g() // expected-error{{capture of non-sendable type 'T.Type' in an isolated closure}}
}
}
nonisolated func passMetaVal<T: Q>(_: T.Type) {
let x = T.self // expected-error{{capture of non-sendable type 'T.Type' in an isolated closure}}
Task.detached {
acceptMeta(x) // expected-error{{capture of non-sendable type}}
}
}
nonisolated func staticCallThroughMeta<T: Q>(_: T.Type) {
Task.detached {
T.g()
T.g() // expected-error{{capture of non-sendable type}}
}
}
nonisolated func passMeta<T: Q>(_: T.Type) {
Task.detached {
acceptMeta(T.self) // expected-error{{capture of non-sendable type 'T.Type' in an isolated closure}}
}
}
nonisolated func staticCallThroughMetaSendable<T: Sendable & Q>(_: T.Type) {
Task.detached {
T.g()
}
}
nonisolated func passMetaSendable<T: Sendable & Q>(_: T.Type) {
Task.detached {
acceptMeta(T.self)
}
}
nonisolated func passMetaWithSendableVal<T: Sendable & Q>(_: T.Type) {
let x = T.self
Task.detached {
acceptMeta(x) // okay, because T is Sendable implies T.Type: Sendable
x.g() // okay, because T is Sendable implies T.Type: Sendable
}
}

View File

@@ -55,7 +55,7 @@ enum TrailingSemi {
};
// The substitution map for a declref should be relatively unobtrusive.
// CHECK-AST-LABEL: (func_decl{{.*}}"generic(_:)" "<T : Hashable>" interface_type="<T where T : Hashable> (T) -> ()" access=internal captures=(<generic> )
// CHECK-AST-LABEL: (func_decl{{.*}}"generic(_:)" "<T : Hashable>" interface_type="<T where T : Hashable> (T) -> ()" access=internal captures=(<generic> {{.*}})
func generic<T: Hashable>(_: T) {}
// CHECK-AST: (pattern_binding_decl
// CHECK-AST: (processed_init=declref_expr type="(Int) -> ()" location={{.*}} range={{.*}} decl="main.(file).generic@{{.*}} [with (substitution_map generic_signature=<T where T : Hashable> T -> Int)]" function_ref=unapplied))

View File

@@ -6,7 +6,7 @@ func doSomething<T>(_ t: T) {}
func outerGeneric<T>(t: T, x: AnyObject) {
// Simple case -- closure captures outer generic parameter
// CHECK: closure_expr type="() -> ()" {{.*}} discriminator=0 nonisolated captures=(<generic> t<direct>) escaping single_expression
// CHECK: closure_expr type="() -> ()" {{.*}} discriminator=0 nonisolated captures=(<generic> t<direct> type=T, type=τ_0_0) escaping single_expression
_ = { doSomething(t) }
// Special case -- closure does not capture outer generic parameters
@@ -15,20 +15,20 @@ func outerGeneric<T>(t: T, x: AnyObject) {
// Special case -- closure captures outer generic parameter, but it does not
// appear as the type of any expression
// CHECK: closure_expr type="() -> ()" {{.*}} discriminator=2 nonisolated captures=(<generic> x<direct>)
// CHECK: closure_expr type="() -> ()" {{.*}} discriminator=2 nonisolated captures=(<generic> x<direct> type=T)
_ = { if x is T {} }
// Nested generic functions always capture outer generic parameters, even if
// they're not mentioned in the function body
// CHECK: func_decl{{.*}}"innerGeneric(u:)" "<U>" interface_type="<T, U> (u: U) -> ()" {{.*}} captures=(<generic> )
// CHECK: func_decl{{.*}}"innerGeneric(u:)" "<U>" interface_type="<T, U> (u: U) -> ()" {{.*}} captures=(<generic> type=τ_1_0)
func innerGeneric<U>(u: U) {}
// Make sure we look through typealiases
typealias TT = (a: T, b: T)
// CHECK: func_decl{{.*}}"localFunction(tt:)" interface_type="<T> (tt: TT) -> ()" {{.*}} captures=(<generic> )
// CHECK: func_decl{{.*}}"localFunction(tt:)" interface_type="<T> (tt: TT) -> ()" {{.*}} captures=(<generic> type=τ_0_0)
func localFunction(tt: TT) {}
// CHECK: closure_expr type="(TT) -> ()" {{.*}} captures=(<generic> )
// CHECK: closure_expr type="(TT) -> ()" {{.*}} captures=(<generic> type=T)
let _: (TT) -> () = { _ in }
}