Skip to content

Commit bfb1f2e

Browse files
committed
[region-isolation] Improve the error we emit for closure literals captured as a sending parameter.
Specifically: I changed the main error message to focus on the closure and that the closure is being accessed concurrently. If we find that we captured a value that is the actual isolation source, we emit that the capture is actually actor isolated. If the captured value is in the same region as the isolated value but is not isolated, we instead say that the value is accessible from *-isolated code or code within the current task. If we find multiple captures and we do not which is the actual value that was in the same region before we formed the partial apply, we just emit a note on the captures saying that the closure captures the value. I changed the diagnostics from using the phrase "task-isolated" to use some variant of accessible to code in the current task. The idea is that in all situations we provide a breadcrumb that the user can start investigating rather than just saying that the closure is "task-isolated". From a preconcurrency perspective, I made it so that we apply the preconcurrency behavior of all of the captures. This means that if one of the captures is preconcurrency, we apply the preconcurrency restriction to the closure. This is one step towards making it so that preconcurrency applies at the region level... we just are not completely there yet. rdar://133798044 (cherry picked from commit 4bb2e4f) Conflicts: include/swift/AST/DiagnosticsSIL.def lib/SILOptimizer/Mandatory/TransferNonSendable.cpp test/Concurrency/concurrent_value_checking.swift test/Concurrency/isolated_parameters.swift test/Concurrency/sendable_preconcurrency.swift test/Concurrency/sendable_without_preconcurrency.swift test/Concurrency/sendable_without_preconcurrency_2.swift test/Concurrency/transfernonsendable.swift test/Concurrency/transfernonsendable_global_actor_sending.swift
1 parent 026ffdd commit bfb1f2e

11 files changed

+551
-28
lines changed

include/swift/AST/DiagnosticsSIL.def

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,23 @@ NOTE(regionbasedisolation_named_isolated_closure_yields_race, none,
10411041
"%0%1 is captured by a %2 closure. %2 uses in closure may race against later %3 uses",
10421042
(StringRef, Identifier, ActorIsolation, ActorIsolation))
10431043

1044+
ERROR(regionbasedisolation_typed_tns_passed_sending_closure, none,
1045+
"passing closure as a 'sending' parameter risks causing data races between %0 and concurrent execution of the closure",
1046+
(StringRef))
1047+
NOTE(regionbasedisolation_typed_tns_passed_to_sending_closure_helper_have_value, none,
1048+
"closure captures %0 %1",
1049+
(StringRef, DeclName))
1050+
NOTE(regionbasedisolation_typed_tns_passed_to_sending_closure_helper_have_value_task_isolated, none,
1051+
"closure captures %0 which is accessible to code in the current task",
1052+
(DeclName))
1053+
NOTE(regionbasedisolation_typed_tns_passed_to_sending_closure_helper_have_value_region, none,
1054+
"closure captures %1 which is accessible to %0 code",
1055+
(StringRef, DeclName))
1056+
1057+
NOTE(regionbasedisolation_typed_tns_passed_to_sending_closure_helper_multiple_value, none,
1058+
"closure captures non-Sendable %0",
1059+
(DeclName))
1060+
10441061
NOTE(regionbasedisolation_named_transfer_nt_asynclet_capture, none,
10451062
"sending %1 %0 into async let risks causing data races between nonisolated and %1 uses",
10461063
(Identifier, StringRef))

include/swift/SILOptimizer/Utils/PartitionUtils.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@ class RepresentativeValue {
141141
return value.get<SILInstruction *>();
142142
}
143143

144+
bool operator==(SILValue other) const {
145+
if (hasRegionIntroducingInst())
146+
return false;
147+
return getValue() == other;
148+
}
149+
144150
SWIFT_DEBUG_DUMP { print(llvm::dbgs()); }
145151

146152
private:

lib/SILOptimizer/Mandatory/TransferNonSendable.cpp

Lines changed: 241 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,29 @@ using Region = PartitionPrimitives::Region;
5858
// MARK: Utilities
5959
//===----------------------------------------------------------------------===//
6060

61+
static SILValue stripFunctionConversions(SILValue val) {
62+
while (true) {
63+
if (auto ti = dyn_cast<ThinToThickFunctionInst>(val)) {
64+
val = ti->getOperand();
65+
continue;
66+
}
67+
68+
if (auto cfi = dyn_cast<ConvertFunctionInst>(val)) {
69+
val = cfi->getOperand();
70+
continue;
71+
}
72+
73+
if (auto cvt = dyn_cast<ConvertEscapeToNoEscapeInst>(val)) {
74+
val = cvt->getOperand();
75+
continue;
76+
}
77+
78+
break;
79+
}
80+
81+
return val;
82+
}
83+
6184
static std::optional<DiagnosticBehavior>
6285
getDiagnosticBehaviorLimitForValue(SILValue value) {
6386
auto *nom = value->getType().getNominalOrBoundGenericNominal();
@@ -72,6 +95,38 @@ getDiagnosticBehaviorLimitForValue(SILValue value) {
7295
return getConcurrencyDiagnosticBehaviorLimit(nom, fromDC);
7396
}
7497

98+
static std::optional<DiagnosticBehavior>
99+
getDiagnosticBehaviorLimitForCapturedValue(CapturedValue value) {
100+
ValueDecl *decl = value.getDecl();
101+
auto *nom = decl->getInterfaceType()->getNominalOrBoundGenericNominal();
102+
if (!nom)
103+
return {};
104+
105+
auto *fromDC = decl->getInnermostDeclContext();
106+
return getConcurrencyDiagnosticBehaviorLimit(nom, fromDC);
107+
}
108+
109+
/// Find the most conservative diagnostic behavior by taking the max over all
110+
/// DiagnosticBehavior for the captured values.
111+
static std::optional<DiagnosticBehavior>
112+
getDiagnosticBehaviorLimitForCapturedValues(
113+
ArrayRef<CapturedValue> capturedValues) {
114+
using UnderlyingType = std::underlying_type<DiagnosticBehavior>::type;
115+
116+
std::optional<DiagnosticBehavior> diagnosticBehavior;
117+
for (auto value : capturedValues) {
118+
auto lhs = UnderlyingType(
119+
diagnosticBehavior.value_or(DiagnosticBehavior::Unspecified));
120+
auto rhs = UnderlyingType(
121+
getDiagnosticBehaviorLimitForCapturedValue(value).value_or(
122+
DiagnosticBehavior::Unspecified));
123+
auto result = DiagnosticBehavior(std::max(lhs, rhs));
124+
if (result != DiagnosticBehavior::Unspecified)
125+
diagnosticBehavior = result;
126+
}
127+
return diagnosticBehavior;
128+
}
129+
75130
static std::optional<SILDeclRef> getDeclRefForCallee(SILInstruction *inst) {
76131
auto fas = FullApplySite::isa(inst);
77132
if (!fas)
@@ -1318,6 +1373,98 @@ class TransferNonTransferrableDiagnosticEmitter {
13181373
.limitBehaviorIf(getBehaviorLimit());
13191374
}
13201375

1376+
/// Only use if we were able to find the actual isolated value.
1377+
void emitTypedSendingNeverSendableToSendingClosureParamDirectlyIsolated(
1378+
SILLocation loc, CapturedValue capturedValue) {
1379+
SmallString<64> descriptiveKindStr;
1380+
{
1381+
llvm::raw_svector_ostream os(descriptiveKindStr);
1382+
if (getIsolationRegionInfo().getIsolationInfo().isTaskIsolated()) {
1383+
os << "code in the current task";
1384+
} else {
1385+
getIsolationRegionInfo().printForDiagnostics(os);
1386+
os << " code";
1387+
}
1388+
}
1389+
1390+
diagnoseError(loc,
1391+
diag::regionbasedisolation_typed_tns_passed_sending_closure,
1392+
descriptiveKindStr)
1393+
.highlight(loc.getSourceRange())
1394+
.limitBehaviorIf(
1395+
getDiagnosticBehaviorLimitForCapturedValue(capturedValue));
1396+
1397+
auto capturedLoc = RegularLocation(capturedValue.getLoc());
1398+
if (getIsolationRegionInfo().getIsolationInfo().isTaskIsolated()) {
1399+
auto diag = diag::
1400+
regionbasedisolation_typed_tns_passed_to_sending_closure_helper_have_value_task_isolated;
1401+
diagnoseNote(capturedLoc, diag, capturedValue.getDecl()->getName());
1402+
return;
1403+
}
1404+
1405+
descriptiveKindStr.clear();
1406+
{
1407+
llvm::raw_svector_ostream os(descriptiveKindStr);
1408+
getIsolationRegionInfo().printForDiagnostics(os);
1409+
}
1410+
1411+
auto diag = diag::
1412+
regionbasedisolation_typed_tns_passed_to_sending_closure_helper_have_value;
1413+
diagnoseNote(capturedLoc, diag, descriptiveKindStr,
1414+
capturedValue.getDecl()->getName());
1415+
}
1416+
1417+
void emitTypedSendingNeverSendableToSendingClosureParam(
1418+
SILLocation loc, ArrayRef<CapturedValue> capturedValues) {
1419+
SmallString<64> descriptiveKindStr;
1420+
{
1421+
llvm::raw_svector_ostream os(descriptiveKindStr);
1422+
if (getIsolationRegionInfo().getIsolationInfo().isTaskIsolated()) {
1423+
os << "code in the current task";
1424+
} else {
1425+
getIsolationRegionInfo().printForDiagnostics(os);
1426+
os << " code";
1427+
}
1428+
}
1429+
1430+
auto behaviorLimit =
1431+
getDiagnosticBehaviorLimitForCapturedValues(capturedValues);
1432+
diagnoseError(loc,
1433+
diag::regionbasedisolation_typed_tns_passed_sending_closure,
1434+
descriptiveKindStr)
1435+
.highlight(loc.getSourceRange())
1436+
.limitBehaviorIf(behaviorLimit);
1437+
1438+
if (capturedValues.size() == 1) {
1439+
auto captured = capturedValues.front();
1440+
auto capturedLoc = RegularLocation(captured.getLoc());
1441+
if (getIsolationRegionInfo().getIsolationInfo().isTaskIsolated()) {
1442+
auto diag = diag::
1443+
regionbasedisolation_typed_tns_passed_to_sending_closure_helper_have_value_task_isolated;
1444+
diagnoseNote(capturedLoc, diag, captured.getDecl()->getName());
1445+
return;
1446+
}
1447+
1448+
descriptiveKindStr.clear();
1449+
{
1450+
llvm::raw_svector_ostream os(descriptiveKindStr);
1451+
getIsolationRegionInfo().printForDiagnostics(os);
1452+
}
1453+
auto diag = diag::
1454+
regionbasedisolation_typed_tns_passed_to_sending_closure_helper_have_value_region;
1455+
diagnoseNote(capturedLoc, diag, descriptiveKindStr,
1456+
captured.getDecl()->getName());
1457+
return;
1458+
}
1459+
1460+
for (auto captured : capturedValues) {
1461+
auto capturedLoc = RegularLocation(captured.getLoc());
1462+
auto diag = diag::
1463+
regionbasedisolation_typed_tns_passed_to_sending_closure_helper_multiple_value;
1464+
diagnoseNote(capturedLoc, diag, captured.getDecl()->getName());
1465+
}
1466+
}
1467+
13211468
void emitNamedOnlyError(SILLocation loc, Identifier name) {
13221469
diagnoseError(loc, diag::regionbasedisolation_named_transfer_yields_race,
13231470
name)
@@ -1455,12 +1602,13 @@ class TransferNonTransferrableDiagnosticEmitter {
14551602
class TransferNonTransferrableDiagnosticInferrer {
14561603
struct AutoClosureWalker;
14571604

1605+
RegionAnalysisValueMap &valueMap;
14581606
TransferNonTransferrableDiagnosticEmitter diagnosticEmitter;
14591607

14601608
public:
14611609
TransferNonTransferrableDiagnosticInferrer(
1462-
TransferredNonTransferrableInfo info)
1463-
: diagnosticEmitter(info) {}
1610+
RegionAnalysisValueMap &valueMap, TransferredNonTransferrableInfo info)
1611+
: valueMap(valueMap), diagnosticEmitter(info) {}
14641612

14651613
/// Gathers diagnostics. Returns false if we emitted a "I don't understand
14661614
/// error". If we emit such an error, we should bail without emitting any
@@ -1474,10 +1622,94 @@ class TransferNonTransferrableDiagnosticInferrer {
14741622
bool initForIsolatedPartialApply(
14751623
Operand *op, AbstractClosureExpr *ace,
14761624
std::optional<ActorIsolation> actualCallerIsolation = {});
1625+
1626+
bool initForSendingPartialApply(FullApplySite fas, Operand *pai);
1627+
1628+
std::optional<unsigned>
1629+
getIsolatedValuePartialApplyIndex(PartialApplyInst *pai,
1630+
SILValue isolatedValue) {
1631+
for (auto &paiOp : ApplySite(pai).getArgumentOperands()) {
1632+
if (valueMap.getTrackableValue(paiOp.get()).getRepresentative() ==
1633+
isolatedValue) {
1634+
// isolated_any causes all partial apply parameters to be shifted by 1
1635+
// due to the implicit isolated any parameter.
1636+
unsigned isIsolatedAny = pai->getFunctionType()->getIsolation() ==
1637+
SILFunctionTypeIsolation::Erased;
1638+
return ApplySite(pai).getAppliedArgIndex(paiOp) - isIsolatedAny;
1639+
}
1640+
}
1641+
1642+
return {};
1643+
}
14771644
};
14781645

14791646
} // namespace
14801647

1648+
bool TransferNonTransferrableDiagnosticInferrer::initForSendingPartialApply(
1649+
FullApplySite fas, Operand *paiOp) {
1650+
auto *pai =
1651+
dyn_cast<PartialApplyInst>(stripFunctionConversions(paiOp->get()));
1652+
if (!pai)
1653+
return false;
1654+
1655+
// For now we want this to be really narrow and to only apply to closure
1656+
// literals.
1657+
auto *ce = pai->getLoc().getAsASTNode<ClosureExpr>();
1658+
if (!ce)
1659+
return false;
1660+
1661+
// Ok, we now know we have a partial apply and it is a closure literal. Lets
1662+
// see if we can find the exact thing that caused the closure literal to be
1663+
// actor isolated.
1664+
auto isolationInfo = diagnosticEmitter.getIsolationRegionInfo();
1665+
if (isolationInfo->hasIsolatedValue()) {
1666+
// Now that we have the value, see if that value is one of our captured
1667+
// values.
1668+
auto isolatedValue = isolationInfo->getIsolatedValue();
1669+
auto matchingElt = getIsolatedValuePartialApplyIndex(pai, isolatedValue);
1670+
if (matchingElt) {
1671+
// Ok, we found the matching element. Lets emit our diagnostic!
1672+
auto capture = ce->getCaptureInfo().getCaptures()[*matchingElt];
1673+
diagnosticEmitter
1674+
.emitTypedSendingNeverSendableToSendingClosureParamDirectlyIsolated(
1675+
ce, capture);
1676+
return true;
1677+
}
1678+
}
1679+
1680+
// Ok, we are not tracking an actual isolated value or we do not capture the
1681+
// isolated value directly... we need to be smarter here. First lets gather up
1682+
// all non-Sendable values captured by the closure.
1683+
SmallVector<CapturedValue, 8> nonSendableCaptures;
1684+
for (auto capture : ce->getCaptureInfo().getCaptures()) {
1685+
auto *decl = capture.getDecl();
1686+
auto type = decl->getInterfaceType()->getCanonicalType();
1687+
auto silType = SILType::getPrimitiveObjectType(type);
1688+
if (!SILIsolationInfo::isNonSendableType(silType, pai->getFunction()))
1689+
continue;
1690+
1691+
auto *fromDC = decl->getInnermostDeclContext();
1692+
auto *nom = silType.getNominalOrBoundGenericNominal();
1693+
if (nom && fromDC) {
1694+
if (auto diagnosticBehavior =
1695+
getConcurrencyDiagnosticBehaviorLimit(nom, fromDC)) {
1696+
if (*diagnosticBehavior == DiagnosticBehavior::Ignore)
1697+
continue;
1698+
}
1699+
}
1700+
nonSendableCaptures.push_back(capture);
1701+
}
1702+
1703+
// If we do not have any non-Sendable captures... bail.
1704+
if (nonSendableCaptures.empty())
1705+
return false;
1706+
1707+
// Otherwise, emit the diagnostic.
1708+
diagnosticEmitter.emitTypedSendingNeverSendableToSendingClosureParam(
1709+
ce, nonSendableCaptures);
1710+
return true;
1711+
}
1712+
14811713
bool TransferNonTransferrableDiagnosticInferrer::initForIsolatedPartialApply(
14821714
Operand *op, AbstractClosureExpr *ace,
14831715
std::optional<ActorIsolation> actualCallerIsolation) {
@@ -1585,6 +1817,11 @@ bool TransferNonTransferrableDiagnosticInferrer::run() {
15851817
if (auto fas = FullApplySite::isa(op->getUser())) {
15861818
if (fas.getArgumentParameterInfo(*op).hasOption(
15871819
SILParameterInfo::Sending)) {
1820+
// Before we do anything, lets see if we are passing a sendable closure
1821+
// literal. If we do, we want to emit a special error that states which
1822+
// captured value caused the actual error.
1823+
if (initForSendingPartialApply(fas, op))
1824+
return true;
15881825

15891826
// See if we can infer a name from the value.
15901827
SmallString<64> resultingName;
@@ -1724,7 +1961,8 @@ void TransferNonSendableImpl::emitTransferredNonTransferrableDiagnostics() {
17241961
llvm::dbgs() << "Emitting transfer non transferrable diagnostics.\n");
17251962

17261963
for (auto info : transferredNonTransferrableInfoList) {
1727-
TransferNonTransferrableDiagnosticInferrer diagnosticInferrer(info);
1964+
TransferNonTransferrableDiagnosticInferrer diagnosticInferrer(
1965+
regionInfo->getValueMap(), info);
17281966
diagnosticInferrer.run();
17291967
}
17301968
}

test/Concurrency/concurrent_value_checking.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -413,20 +413,20 @@ extension NotConcurrent {
413413
func f() { }
414414

415415
func test() {
416-
Task { // expected-tns-warning {{task-isolated value of type '() async -> ()' passed as a strongly transferred parameter}}
417-
f()
416+
Task { // expected-tns-warning {{passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure}}
417+
f() // expected-tns-note {{closure captures 'self' which is accessible to code in the current task}}
418418
}
419419

420-
Task { // expected-tns-warning {{task-isolated value of type '() async -> ()' passed as a strongly transferred parameter}}
421-
self.f()
420+
Task { // expected-tns-warning {{passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure}}
421+
self.f() // expected-tns-note {{closure captures 'self' which is accessible to code in the current task}}
422422
}
423423

424-
Task { [self] in // expected-tns-warning {{task-isolated value of type '() async -> ()' passed as a strongly transferred parameter}}
425-
f()
424+
Task { [self] in // expected-tns-warning {{passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure}}
425+
f() // expected-tns-note {{closure captures 'self' which is accessible to code in the current task}}
426426
}
427427

428-
Task { [self] in // expected-tns-warning {{task-isolated value of type '() async -> ()' passed as a strongly transferred parameter}}
429-
self.f()
428+
Task { [self] in // expected-tns-warning {{passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure}}
429+
self.f() // expected-tns-note {{closure captures 'self' which is accessible to code in the current task}}
430430
}
431431
}
432432
}

test/Concurrency/isolated_parameters.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -501,9 +501,9 @@ func testNonSendableCaptures(ns: NotSendable, a: isolated MyActor) {
501501

502502
// FIXME: The `a` in the capture list and `isolated a` are the same,
503503
// but the actor isolation checker doesn't know that.
504-
Task { [a] in // expected-tns-warning {{'a'-isolated value of type '() async -> ()' passed as a strongly transferred parameter}}
504+
Task { [a] in // expected-tns-warning {{passing closure as a 'sending' parameter risks causing data races between 'a'-isolated code and concurrent execution of the closure}}
505505
_ = a
506-
_ = ns
506+
_ = ns // expected-tns-note {{closure captures 'a'-isolated 'ns'}}
507507
}
508508
}
509509

test/Concurrency/sendable_preconcurrency.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,13 @@ struct MyType3 {
3131
}
3232

3333
func testA(ns: NS, mt: MyType, mt2: MyType2, mt3: MyType3, sc: StrictClass, nsc: NonStrictClass) async {
34-
// This is task isolated since we are capturing function arguments.
35-
Task { // expected-tns-warning {{task-isolated value of type '() async -> ()' passed as a strongly transferred parameter}}
34+
// This is task isolated since we are capturing function arguments... but
35+
// since we are merging NonStrictClass from a preconcurrency module, the whole
36+
// error is squelched since we allow for preconcurrency to apply to the entire
37+
// capture list.
38+
Task {
3639
print(ns)
37-
print(mt) // no warning: MyType is Sendable because we suppressed NonStrictClass's warning
40+
print(mt)
3841
print(mt2)
3942
print(mt3)
4043
print(sc)

0 commit comments

Comments
 (0)