[SE-0470] Track the potential introduction of isolated conformances in regions

When we introduce isolation due to a (potential) isolated conformance,
keep track of the protocol to which the conformance could be
introduced. Use this information for two reasons:

1. Downgrade the error to a warning in Swift < 7, because we are newly
diagnosing these
2. Add a note indicating where the isolated conformance could be introduced.
This commit is contained in:
Doug Gregor
2025-07-08 11:13:50 -07:00
parent c33ce3f029
commit 02c34bb830
6 changed files with 132 additions and 56 deletions

View File

@@ -1098,6 +1098,10 @@ NOTE(regionbasedisolation_typed_sendneversendable_via_arg_callee, none,
"sending %0 value of non-Sendable type %1 to %2 %kind3 risks causing races in between %0 and %2 uses",
(StringRef, Type, StringRef, const ValueDecl *))
NOTE(regionbasedisolation_isolated_conformance_introduced, none,
"isolated conformance to %kind0 can be introduced here",
(const ValueDecl *))
// Error that is only used when the send non sendable emitter cannot discover any
// information to give a better diagnostic.
ERROR(regionbasedisolation_task_or_actor_isolated_sent, none,

View File

@@ -190,7 +190,7 @@ public:
NonisolatedNonsendingTaskIsolated = 0x4,
/// The maximum number of bits used by a Flag.
MaxNumBits = 2,
MaxNumBits = 3,
};
using Options = OptionSet<Flag>;
@@ -208,13 +208,19 @@ private:
/// derived isolatedValue from.
ActorInstance actorInstance;
/// When the isolation is introduced due to a (potentially) isolated
/// conformance, the protocol whose conformance might be isolated.
ProtocolDecl *isolatedConformance = nullptr;
unsigned kind : 8;
unsigned options : 8;
SILIsolationInfo(SILValue isolatedValue, SILValue actorInstance,
ActorIsolation actorIsolation, Options options = Options())
ActorIsolation actorIsolation, Options options = Options(),
ProtocolDecl *isolatedConformance = nullptr)
: actorIsolation(actorIsolation), isolatedValue(isolatedValue),
actorInstance(ActorInstance::getForValue(actorInstance)), kind(Actor),
actorInstance(ActorInstance::getForValue(actorInstance)),
isolatedConformance(isolatedConformance), kind(Actor),
options(options.toRaw()) {
assert((!actorInstance ||
(actorIsolation.getKind() == ActorIsolation::ActorInstance &&
@@ -226,15 +232,20 @@ private:
}
SILIsolationInfo(SILValue isolatedValue, ActorInstance actorInstance,
ActorIsolation actorIsolation, Options options = Options())
ActorIsolation actorIsolation, Options options = Options(),
ProtocolDecl *isolatedConformance = nullptr)
: actorIsolation(actorIsolation), isolatedValue(isolatedValue),
actorInstance(actorInstance), kind(Actor), options(options.toRaw()) {
actorInstance(actorInstance), isolatedConformance(isolatedConformance),
kind(Actor), options(options.toRaw())
{
assert(actorInstance);
assert(actorIsolation.getKind() == ActorIsolation::ActorInstance);
}
SILIsolationInfo(Kind kind, SILValue isolatedValue)
: actorIsolation(), isolatedValue(isolatedValue), kind(kind), options(0) {
SILIsolationInfo(Kind kind, SILValue isolatedValue,
ProtocolDecl *isolatedConformance = nullptr)
: actorIsolation(), isolatedValue(isolatedValue),
isolatedConformance(isolatedConformance), kind(kind), options(0) {
}
SILIsolationInfo(Kind kind, Options options = Options())
@@ -261,6 +272,12 @@ public:
return getOptions().contains(Flag::UnsafeNonIsolated);
}
// Retrieve the protocol to which there is (or could be) an isolated
// conformance.
ProtocolDecl *getIsolatedConformance() const {
return isolatedConformance;
}
SILIsolationInfo withUnsafeNonIsolated(bool newValue = true) const {
assert(*this && "Cannot be unknown");
auto self = *this;
@@ -292,6 +309,26 @@ public:
return self;
}
/// Produce a new isolation info value that merges in the given isolated
/// conformance value.
///
/// If both isolation infos have an isolation conformance, pick one
/// arbitrarily. Otherwise, the result has no isolated conformance.
SILIsolationInfo
withMergedIsolatedConformance(ProtocolDecl *newIsolatedConformance) const {
SILIsolationInfo result(*this);
if (!isolatedConformance || !newIsolatedConformance) {
result.isolatedConformance = nullptr;
return result;
}
result.isolatedConformance =
ProtocolDecl::compare(isolatedConformance, newIsolatedConformance) <= 0
? isolatedConformance
: newIsolatedConformance;
return result;
}
/// Returns true if this actor isolation is derived from an unapplied
/// isolation parameter. When merging, we allow for this to be merged with a
/// more specific isolation kind.
@@ -458,10 +495,13 @@ public:
Flag::UnappliedIsolatedAnyParameter};
}
static SILIsolationInfo getGlobalActorIsolated(SILValue value,
Type globalActorType) {
static SILIsolationInfo getGlobalActorIsolated(
SILValue value,
Type globalActorType,
ProtocolDecl *isolatedConformance = nullptr) {
return {value, SILValue() /*no actor instance*/,
ActorIsolation::forGlobalActor(globalActorType)};
ActorIsolation::forGlobalActor(globalActorType),
Options(), isolatedConformance};
}
static SILIsolationInfo getGlobalActorIsolated(SILValue value,
@@ -473,8 +513,9 @@ public:
isolation.getGlobalActor());
}
static SILIsolationInfo getTaskIsolated(SILValue value) {
return {Kind::Task, value};
static SILIsolationInfo getTaskIsolated(
SILValue value, ProtocolDecl *isolatedConformance = nullptr) {
return {Kind::Task, value, isolatedConformance};
}
/// Attempt to infer the isolation region info for \p inst.

View File

@@ -297,6 +297,9 @@ static SILIsolationInfo getIsolationForCastConformances(
const auto &destLayout = destType.getExistentialLayout();
for (auto proto : destLayout.getProtocols()) {
if (proto->isMarkerProtocol())
continue;
// If the source type already conforms to the protocol, we won't be looking
// it up dynamically.
if (!lookupConformance(sourceType, proto, /*allowMissing=*/false).isInvalid())
@@ -313,12 +316,12 @@ static SILIsolationInfo getIsolationForCastConformances(
// Otherwise, it's task-isolated.
if (functionIsolation && functionIsolation->isGlobalActor()) {
return SILIsolationInfo::getGlobalActorIsolated(
value, functionIsolation->getGlobalActor());
value, functionIsolation->getGlobalActor(), proto);
}
// Consider the cast to be task-isolated, because the runtime could find
// a conformance that is isolated to the current context.
return SILIsolationInfo::getTaskIsolated(value);
return SILIsolationInfo::getTaskIsolated(value, proto);
}
return {};
@@ -4131,6 +4134,9 @@ SILIsolationInfo
PartitionOpTranslator::getIsolationFromConformances(
SILValue value, ArrayRef<ProtocolConformanceRef> conformances) {
for (auto conformance: conformances) {
if (conformance.getProtocol()->isMarkerProtocol())
continue;
// If the conformance is a pack, recurse.
if (conformance.isPack()) {
auto pack = conformance.getPack();
@@ -4149,7 +4155,7 @@ PartitionOpTranslator::getIsolationFromConformances(
auto isolation = conformance.getConcrete()->getIsolation();
if (isolation.isGlobalActor()) {
return SILIsolationInfo::getGlobalActorIsolated(
value, isolation.getGlobalActor());
value, isolation.getGlobalActor(), conformance.getProtocol());
}
continue;
@@ -4164,7 +4170,8 @@ PartitionOpTranslator::getIsolationFromConformances(
if (sendableMetatype &&
lookupConformance(conformance.getType(), sendableMetatype,
/*allowMissing=*/false).isInvalid()) {
return SILIsolationInfo::getTaskIsolated(value);
return SILIsolationInfo::getTaskIsolated(value,
conformance.getProtocol());
}
}
}
@@ -4175,7 +4182,7 @@ PartitionOpTranslator::getIsolationFromConformances(
TranslationSemantics
PartitionOpTranslator::visitInitExistentialAddrInst(InitExistentialAddrInst *ieai) {
auto conformanceIsolationInfo = getIsolationFromConformances(
ieai->getResult(0), ieai->getConformances());
ieai, ieai->getConformances());
translateSILMultiAssign(ieai->getResults(),
makeOperandRefRange(ieai->getAllOperands()),
@@ -4187,7 +4194,7 @@ PartitionOpTranslator::visitInitExistentialAddrInst(InitExistentialAddrInst *iea
TranslationSemantics
PartitionOpTranslator::visitInitExistentialRefInst(InitExistentialRefInst *ieri) {
auto conformanceIsolationInfo = getIsolationFromConformances(
ieri->getResult(0), ieri->getConformances());
ieri, ieri->getConformances());
translateSILMultiAssign(ieri->getResults(),
makeOperandRefRange(ieri->getAllOperands()),
@@ -4199,7 +4206,7 @@ PartitionOpTranslator::visitInitExistentialRefInst(InitExistentialRefInst *ieri)
TranslationSemantics
PartitionOpTranslator::visitInitExistentialValueInst(InitExistentialValueInst *ievi) {
auto conformanceIsolationInfo = getIsolationFromConformances(
ievi->getResult(0), ievi->getConformances());
ievi, ievi->getConformances());
translateSILMultiAssign(ievi->getResults(),
makeOperandRefRange(ievi->getAllOperands()),

View File

@@ -1312,6 +1312,18 @@ public:
~SendNeverSentDiagnosticEmitter() {
if (!emittedErrorDiagnostic) {
emitUnknownPatternError();
} else if (auto proto = isolationRegionInfo.getIsolationInfo()
.getIsolatedConformance()) {
// If the diagnostic comes from a (potentially) isolated conformance,
// add a note saying so and indicating where the isolated conformance
// can come in.
if (auto value = isolationRegionInfo.getIsolationInfo().getIsolatedValue()) {
if (auto loc = value.getLoc()) {
diagnoseNote(
loc, diag::regionbasedisolation_isolated_conformance_introduced,
proto);
}
}
}
}
@@ -1326,6 +1338,13 @@ public:
}
std::optional<DiagnosticBehavior> getBehaviorLimit() const {
// If the failure is due to an isolated conformance, downgrade the error
// to a warning prior to Swift 7.
if (isolationRegionInfo.getIsolationInfo().getIsolatedConformance() &&
!sendingOperand->get()->getType().getASTType()->getASTContext().LangOpts
.isSwiftVersionAtLeast(7))
return DiagnosticBehavior::Warning;
return sendingOperand->get()->getType().getConcurrencyDiagnosticBehavior(
getOperand()->getFunction());
}
@@ -1577,8 +1596,7 @@ public:
getIsolationRegionInfo().printForDiagnostics(getFunction());
diagnoseNote(loc, diag::regionbasedisolation_named_send_nt_asynclet_capture,
name, descriptiveKindStr)
.limitBehaviorIf(getBehaviorLimit());
name, descriptiveKindStr);
}
void emitNamedIsolation(SILLocation loc, Identifier name,

View File

@@ -1070,6 +1070,10 @@ SILIsolationInfo SILIsolationInfo::get(SILArgument *arg) {
}
void SILIsolationInfo::printOptions(llvm::raw_ostream &os) const {
if (isolatedConformance) {
os << "isolated-conformance-to(" << isolatedConformance->getName() << ")";
}
auto opts = getOptions();
if (!opts)
return;
@@ -1255,9 +1259,11 @@ void SILIsolationInfo::Profile(llvm::FoldingSetNodeID &id) const {
return;
case Task:
id.AddPointer(getIsolatedValue());
id.AddPointer(getIsolatedConformance());
return;
case Actor:
id.AddPointer(getIsolatedValue());
id.AddPointer(getIsolatedConformance());
getActorIsolation().Profile(id);
return;
}
@@ -1570,7 +1576,7 @@ SILDynamicMergedIsolationInfo::merge(SILIsolationInfo other) const {
// If both innerInfo and other have the same isolation, we are obviously
// done. Just return innerInfo since we could return either.
if (innerInfo.hasSameIsolation(other))
return {innerInfo};
return {innerInfo.withMergedIsolatedConformance(other.getIsolatedConformance())};
// Ok, there is some difference in between innerInfo and other. Lets see if
// they are both actor instance isolated and if either are unapplied
@@ -1579,9 +1585,9 @@ SILDynamicMergedIsolationInfo::merge(SILIsolationInfo other) const {
if (innerInfo.getActorIsolation().isActorInstanceIsolated() &&
other.getActorIsolation().isActorInstanceIsolated()) {
if (innerInfo.isUnappliedIsolatedAnyParameter())
return other;
return other.withMergedIsolatedConformance(innerInfo.getIsolatedConformance());
if (other.isUnappliedIsolatedAnyParameter())
return innerInfo;
return innerInfo.withMergedIsolatedConformance(other.getIsolatedConformance());
}
// Otherwise, they do not match... so return None to signal merge failure.
@@ -1610,7 +1616,8 @@ SILDynamicMergedIsolationInfo::merge(SILIsolationInfo other) const {
if (innerInfo.isTaskIsolated() && other.isTaskIsolated()) {
if (innerInfo.isNonisolatedNonsendingTaskIsolated() ||
other.isNonisolatedNonsendingTaskIsolated())
return other.withNonisolatedNonsendingTaskIsolated(true);
return other.withNonisolatedNonsendingTaskIsolated(true)
.withMergedIsolatedConformance(innerInfo.getIsolatedConformance());
}
// Otherwise, just return other.

View File

@@ -120,20 +120,21 @@ func acceptSendingAnyObjectR(_ s: sending AnyObject & R) { }
@MainActor func passSendingExistential<T: P, U: AnyObject & P , V: R, W: AnyObject & R>(
t: sending T, u: sending U, v: sending V, w: sending W
) {
// FIXME: Diagnostics here should really mention that it's the main-actor-
// isolated conformance to P that's the problem.
acceptSendingP(S()) // expected-error{{sending value of non-Sendable type 'S' risks causing data races}}
acceptSendingP(S()) // expected-warning{{sending value of non-Sendable type 'S' risks causing data races}}
// expected-note@-1{{Passing main actor-isolated value of non-Sendable type 'S' as a 'sending' parameter to global function 'acceptSendingP' risks causing races}}
acceptSendingP(SC()) // expected-error{{sending value of non-Sendable type 'SC'}}
// expected-note@-2{{isolated conformance to protocol 'P' can be introduced here}}
acceptSendingP(SC()) // expected-warning{{sending value of non-Sendable type 'SC'}}
// expected-note@-1{{Passing main actor-isolated value of non-Sendable type 'SC' as a 'sending' parameter to global function 'acceptSendingP' risks causing races}}' risks causing data races}}
acceptSendingAnyObjectP(SC()) // expected-error{{sending value of non-Sendable type 'SC' risks causing data races}}
// expected-note@-2{{isolated conformance to protocol 'P' can be introduced here}}
acceptSendingAnyObjectP(SC()) // expected-warning{{sending value of non-Sendable type 'SC' risks causing data races}}
// expected-note@-1{{Passing main actor-isolated value of non-Sendable type 'SC' as a 'sending' parameter to global function 'acceptSendingAnyObjectP' risks causing races}}' risks causing data races}}
acceptSendingP(t) // expected-error{{sending 't' risks causing data races}}
// expected-note@-2{{isolated conformance to protocol 'P' can be introduced here}}
acceptSendingP(t) // expected-warning{{sending 't' risks causing data races}}
// expected-note@-1{{task-isolated 't' is passed as a 'sending' parameter; Uses in callee may race with later task-isolated uses}}
acceptSendingAnyObjectP(u) // expected-error{{sending value of non-Sendable type 'U' risks causing data races}}
// expected-note@-2{{isolated conformance to protocol 'P' can be introduced he}}
acceptSendingAnyObjectP(u) // expected-warning{{sending value of non-Sendable type 'U' risks causing data races}}
// expected-note@-1{{task-isolated value of non-Sendable type 'U'}}
// expected-note@-2{{isolated conformance to protocol 'P' can be introduced here}}
// All of these are okay, because there are no isolated conformances to R.
acceptSendingR(S())
acceptSendingR(SC())
@@ -146,16 +147,15 @@ func dynamicCastingExistential(
_ s1: sending Any,
_ s2: sending Any
) {
// TODO: Improve diagnostics due to isolated conformances.
if let s1p = s1 as? any P {
acceptSendingP(s1p) // expected-error{{sending 's1p' risks causing data races}}
if let s1p = s1 as? any P { // expected-note{{isolated conformance to protocol 'P' can be introduced here}}
acceptSendingP(s1p) // expected-warning{{sending 's1p' risks causing data races}}
// expected-note@-1{{task-isolated 's1p' is passed as a 'sending' parameter; Uses in callee may race with later task-isolated uses}}
} else {
print(s1)
}
if let s2p = s2 as? any AnyObject & P {
acceptSendingAnyObjectP(s2p) // expected-error{{sending 's2p' risks causing data races}}
if let s2p = s2 as? any AnyObject & P { // expected-note{{isolated conformance to protocol 'P' can be introduced here}}
acceptSendingAnyObjectP(s2p) // expected-warning{{sending 's2p' risks causing data races}}
// expected-note@-1{{task-isolated 's2p' is passed as a 'sending' parameter; Uses in callee may race with later task-isolated uses}}
} else {
print(s2)
@@ -185,16 +185,15 @@ func dynamicCastingGeneric(
_ s1: sending some Any,
_ s2: sending some Any
) {
// TODO: Improve diagnostics due to isolated conformances.
if let s1p = s1 as? any P {
acceptSendingP(s1p) // expected-error{{sending 's1p' risks causing data races}}
if let s1p = s1 as? any P { // expected-note{{isolated conformance to protocol 'P' can be introduced here}}
acceptSendingP(s1p) // expected-warning{{sending 's1p' risks causing data races}}
// expected-note@-1{{task-isolated 's1p' is passed as a 'sending' parameter; Uses in callee may race with later task-isolated uses}}
} else {
print(s1)
}
if let s2p = s2 as? any AnyObject & P {
acceptSendingAnyObjectP(s2p) // expected-error{{sending 's2p' risks causing data races}}
if let s2p = s2 as? any AnyObject & P { // expected-note{{isolated conformance to protocol 'P' can be introduced here}}
acceptSendingAnyObjectP(s2p) // expected-warning{{sending 's2p' risks causing data races}}
// expected-note@-1{{task-isolated 's2p' is passed as a 'sending' parameter; Uses in callee may race with later task-isolated uses}}
} else {
print(s2)
@@ -239,12 +238,12 @@ func forceCastingExistential(
_ s1: sending Any,
_ s2: sending AnyObject
) {
let s1p = s1 as! any P
acceptSendingP(s1p) // expected-error{{sending 's1p' risks causing data races}}
let s1p = s1 as! any P // expected-note{{isolated conformance to protocol 'P' can be introduced here}}
acceptSendingP(s1p) // expected-warning{{sending 's1p' risks causing data races}}
// expected-note@-1{{task-isolated 's1p' is passed as a 'sending' parameter; Uses in callee may race with later task-isolated uses}}
let s2p = s2 as! any AnyObject & P
acceptSendingAnyObjectP(s2p) // expected-error{{sending 's2p' risks causing data races}}
let s2p = s2 as! any AnyObject & P // expected-note{{isolated conformance to protocol 'P' can be introduced here}}
acceptSendingAnyObjectP(s2p) // expected-warning{{sending 's2p' risks causing data races}}
// expected-note@-1{{task-isolated 's2p' is passed as a 'sending' parameter; Uses in callee may race with later task-isolated uses}}
}
@@ -263,12 +262,12 @@ func forceCastingGeneric(
_ s1: sending some Any,
_ s2: sending some AnyObject
) {
let s1p = s1 as! any P
acceptSendingP(s1p) // expected-error{{sending 's1p' risks causing data races}}
let s1p = s1 as! any P // expected-note{{isolated conformance to protocol 'P' can be introduced here}}
acceptSendingP(s1p) // expected-warning{{sending 's1p' risks causing data races}}
// expected-note@-1{{task-isolated 's1p' is passed as a 'sending' parameter; Uses in callee may race with later task-isolated uses}}
let s2p = s2 as! any AnyObject & P
acceptSendingAnyObjectP(s2p) // expected-error{{sending 's2p' risks causing data races}}
let s2p = s2 as! any AnyObject & P // expected-note{{isolated conformance to protocol 'P' can be introduced here}}
acceptSendingAnyObjectP(s2p) // expected-warning{{sending 's2p' risks causing data races}}
// expected-note@-1{{task-isolated 's2p' is passed as a 'sending' parameter; Uses in callee may race with later task-isolated uses}}
}
@@ -276,12 +275,12 @@ func forceCastingGeneric(
_ s1: sending some Any,
_ s2: sending some AnyObject
) {
let s1p = s1 as! any P
acceptSendingP(s1p) // expected-error{{sending 's1p' risks causing data races}}
let s1p = s1 as! any P // expected-note{{isolated conformance to protocol 'P' can be introduced here}}
acceptSendingP(s1p) // expected-warning{{sending 's1p' risks causing data races}}
// expected-note@-1{{main actor-isolated 's1p' is passed as a 'sending' parameter; Uses in callee may race with later main actor-isolated uses}}
let s2p = s2 as! any AnyObject & P
acceptSendingAnyObjectP(s2p) // expected-error{{sending 's2p' risks causing data races}}
let s2p = s2 as! any AnyObject & P // expected-note{{isolated conformance to protocol 'P' can be introduced here}}
acceptSendingAnyObjectP(s2p) // expected-warning{{sending 's2p' risks causing data races}}
// expected-note@-1{{main actor-isolated 's2p' is passed as a 'sending' parameter; Uses in callee may race with later main actor-isolated uses}}
}