Skip to content

Commit 4bb2e4f

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
1 parent 328e8af commit 4bb2e4f

12 files changed

+553
-38
lines changed

include/swift/AST/DiagnosticsSIL.def

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,6 +1025,23 @@ NOTE(regionbasedisolation_typed_tns_passed_to_sending, none,
10251025
"causing races inbetween %0 uses and uses reachable from the callee",
10261026
(StringRef, Type))
10271027

1028+
ERROR(regionbasedisolation_typed_tns_passed_sending_closure, none,
1029+
"passing closure as a 'sending' parameter risks causing data races between %0 and concurrent execution of the closure",
1030+
(StringRef))
1031+
NOTE(regionbasedisolation_typed_tns_passed_to_sending_closure_helper_have_value, none,
1032+
"closure captures %0 %1",
1033+
(StringRef, DeclName))
1034+
NOTE(regionbasedisolation_typed_tns_passed_to_sending_closure_helper_have_value_task_isolated, none,
1035+
"closure captures %0 which is accessible to code in the current task",
1036+
(DeclName))
1037+
NOTE(regionbasedisolation_typed_tns_passed_to_sending_closure_helper_have_value_region, none,
1038+
"closure captures %1 which is accessible to %0 code",
1039+
(StringRef, DeclName))
1040+
1041+
NOTE(regionbasedisolation_typed_tns_passed_to_sending_closure_helper_multiple_value, none,
1042+
"closure captures non-Sendable %0",
1043+
(DeclName))
1044+
10281045
NOTE(regionbasedisolation_typed_tns_passed_to_sending_callee, none,
10291046
"Passing %0 value of non-Sendable type %1 as a 'sending' parameter to %2 %3 risks causing races inbetween %0 uses and uses reachable from %3",
10301047
(StringRef, Type, DescriptiveDeclKind, DeclName))

include/swift/SILOptimizer/Utils/PartitionUtils.h

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

158+
bool operator==(SILValue other) const {
159+
if (hasRegionIntroducingInst())
160+
return false;
161+
return getValue() == other;
162+
}
163+
158164
SWIFT_DEBUG_DUMP { print(llvm::dbgs()); }
159165

160166
private:

lib/SILOptimizer/Mandatory/TransferNonSendable.cpp

Lines changed: 242 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,29 @@ static llvm::cl::opt<bool> ForceTypedErrors(
6969
// MARK: Utilities
7070
//===----------------------------------------------------------------------===//
7171

72+
static SILValue stripFunctionConversions(SILValue val) {
73+
while (true) {
74+
if (auto ti = dyn_cast<ThinToThickFunctionInst>(val)) {
75+
val = ti->getOperand();
76+
continue;
77+
}
78+
79+
if (auto cfi = dyn_cast<ConvertFunctionInst>(val)) {
80+
val = cfi->getOperand();
81+
continue;
82+
}
83+
84+
if (auto cvt = dyn_cast<ConvertEscapeToNoEscapeInst>(val)) {
85+
val = cvt->getOperand();
86+
continue;
87+
}
88+
89+
break;
90+
}
91+
92+
return val;
93+
}
94+
7295
static std::optional<DiagnosticBehavior>
7396
getDiagnosticBehaviorLimitForValue(SILValue value) {
7497
auto *nom = value->getType().getNominalOrBoundGenericNominal();
@@ -83,6 +106,38 @@ getDiagnosticBehaviorLimitForValue(SILValue value) {
83106
return getConcurrencyDiagnosticBehaviorLimit(nom, fromDC);
84107
}
85108

109+
static std::optional<DiagnosticBehavior>
110+
getDiagnosticBehaviorLimitForCapturedValue(CapturedValue value) {
111+
ValueDecl *decl = value.getDecl();
112+
auto *nom = decl->getInterfaceType()->getNominalOrBoundGenericNominal();
113+
if (!nom)
114+
return {};
115+
116+
auto *fromDC = decl->getInnermostDeclContext();
117+
return getConcurrencyDiagnosticBehaviorLimit(nom, fromDC);
118+
}
119+
120+
/// Find the most conservative diagnostic behavior by taking the max over all
121+
/// DiagnosticBehavior for the captured values.
122+
static std::optional<DiagnosticBehavior>
123+
getDiagnosticBehaviorLimitForCapturedValues(
124+
ArrayRef<CapturedValue> capturedValues) {
125+
using UnderlyingType = std::underlying_type<DiagnosticBehavior>::type;
126+
127+
std::optional<DiagnosticBehavior> diagnosticBehavior;
128+
for (auto value : capturedValues) {
129+
auto lhs = UnderlyingType(
130+
diagnosticBehavior.value_or(DiagnosticBehavior::Unspecified));
131+
auto rhs = UnderlyingType(
132+
getDiagnosticBehaviorLimitForCapturedValue(value).value_or(
133+
DiagnosticBehavior::Unspecified));
134+
auto result = DiagnosticBehavior(std::max(lhs, rhs));
135+
if (result != DiagnosticBehavior::Unspecified)
136+
diagnosticBehavior = result;
137+
}
138+
return diagnosticBehavior;
139+
}
140+
86141
static std::optional<SILDeclRef> getDeclRefForCallee(SILInstruction *inst) {
87142
auto fas = FullApplySite::isa(inst);
88143
if (!fas)
@@ -1390,6 +1445,98 @@ class TransferNonTransferrableDiagnosticEmitter {
13901445
}
13911446
}
13921447

1448+
/// Only use if we were able to find the actual isolated value.
1449+
void emitTypedSendingNeverSendableToSendingClosureParamDirectlyIsolated(
1450+
SILLocation loc, CapturedValue capturedValue) {
1451+
SmallString<64> descriptiveKindStr;
1452+
{
1453+
llvm::raw_svector_ostream os(descriptiveKindStr);
1454+
if (getIsolationRegionInfo().getIsolationInfo().isTaskIsolated()) {
1455+
os << "code in the current task";
1456+
} else {
1457+
getIsolationRegionInfo().printForDiagnostics(os);
1458+
os << " code";
1459+
}
1460+
}
1461+
1462+
diagnoseError(loc,
1463+
diag::regionbasedisolation_typed_tns_passed_sending_closure,
1464+
descriptiveKindStr)
1465+
.highlight(loc.getSourceRange())
1466+
.limitBehaviorIf(
1467+
getDiagnosticBehaviorLimitForCapturedValue(capturedValue));
1468+
1469+
auto capturedLoc = RegularLocation(capturedValue.getLoc());
1470+
if (getIsolationRegionInfo().getIsolationInfo().isTaskIsolated()) {
1471+
auto diag = diag::
1472+
regionbasedisolation_typed_tns_passed_to_sending_closure_helper_have_value_task_isolated;
1473+
diagnoseNote(capturedLoc, diag, capturedValue.getDecl()->getName());
1474+
return;
1475+
}
1476+
1477+
descriptiveKindStr.clear();
1478+
{
1479+
llvm::raw_svector_ostream os(descriptiveKindStr);
1480+
getIsolationRegionInfo().printForDiagnostics(os);
1481+
}
1482+
1483+
auto diag = diag::
1484+
regionbasedisolation_typed_tns_passed_to_sending_closure_helper_have_value;
1485+
diagnoseNote(capturedLoc, diag, descriptiveKindStr,
1486+
capturedValue.getDecl()->getName());
1487+
}
1488+
1489+
void emitTypedSendingNeverSendableToSendingClosureParam(
1490+
SILLocation loc, ArrayRef<CapturedValue> capturedValues) {
1491+
SmallString<64> descriptiveKindStr;
1492+
{
1493+
llvm::raw_svector_ostream os(descriptiveKindStr);
1494+
if (getIsolationRegionInfo().getIsolationInfo().isTaskIsolated()) {
1495+
os << "code in the current task";
1496+
} else {
1497+
getIsolationRegionInfo().printForDiagnostics(os);
1498+
os << " code";
1499+
}
1500+
}
1501+
1502+
auto behaviorLimit =
1503+
getDiagnosticBehaviorLimitForCapturedValues(capturedValues);
1504+
diagnoseError(loc,
1505+
diag::regionbasedisolation_typed_tns_passed_sending_closure,
1506+
descriptiveKindStr)
1507+
.highlight(loc.getSourceRange())
1508+
.limitBehaviorIf(behaviorLimit);
1509+
1510+
if (capturedValues.size() == 1) {
1511+
auto captured = capturedValues.front();
1512+
auto capturedLoc = RegularLocation(captured.getLoc());
1513+
if (getIsolationRegionInfo().getIsolationInfo().isTaskIsolated()) {
1514+
auto diag = diag::
1515+
regionbasedisolation_typed_tns_passed_to_sending_closure_helper_have_value_task_isolated;
1516+
diagnoseNote(capturedLoc, diag, captured.getDecl()->getName());
1517+
return;
1518+
}
1519+
1520+
descriptiveKindStr.clear();
1521+
{
1522+
llvm::raw_svector_ostream os(descriptiveKindStr);
1523+
getIsolationRegionInfo().printForDiagnostics(os);
1524+
}
1525+
auto diag = diag::
1526+
regionbasedisolation_typed_tns_passed_to_sending_closure_helper_have_value_region;
1527+
diagnoseNote(capturedLoc, diag, descriptiveKindStr,
1528+
captured.getDecl()->getName());
1529+
return;
1530+
}
1531+
1532+
for (auto captured : capturedValues) {
1533+
auto capturedLoc = RegularLocation(captured.getLoc());
1534+
auto diag = diag::
1535+
regionbasedisolation_typed_tns_passed_to_sending_closure_helper_multiple_value;
1536+
diagnoseNote(capturedLoc, diag, captured.getDecl()->getName());
1537+
}
1538+
}
1539+
13931540
void emitNamedOnlyError(SILLocation loc, Identifier name) {
13941541
diagnoseError(loc, diag::regionbasedisolation_named_transfer_yields_race,
13951542
name)
@@ -1527,12 +1674,13 @@ class TransferNonTransferrableDiagnosticEmitter {
15271674
class TransferNonTransferrableDiagnosticInferrer {
15281675
struct AutoClosureWalker;
15291676

1677+
RegionAnalysisValueMap &valueMap;
15301678
TransferNonTransferrableDiagnosticEmitter diagnosticEmitter;
15311679

15321680
public:
15331681
TransferNonTransferrableDiagnosticInferrer(
1534-
TransferredNonTransferrableInfo info)
1535-
: diagnosticEmitter(info) {}
1682+
RegionAnalysisValueMap &valueMap, TransferredNonTransferrableInfo info)
1683+
: valueMap(valueMap), diagnosticEmitter(info) {}
15361684

15371685
/// Gathers diagnostics. Returns false if we emitted a "I don't understand
15381686
/// error". If we emit such an error, we should bail without emitting any
@@ -1546,10 +1694,94 @@ class TransferNonTransferrableDiagnosticInferrer {
15461694
bool initForIsolatedPartialApply(
15471695
Operand *op, AbstractClosureExpr *ace,
15481696
std::optional<ActorIsolation> actualCallerIsolation = {});
1697+
1698+
bool initForSendingPartialApply(FullApplySite fas, Operand *pai);
1699+
1700+
std::optional<unsigned>
1701+
getIsolatedValuePartialApplyIndex(PartialApplyInst *pai,
1702+
SILValue isolatedValue) {
1703+
for (auto &paiOp : ApplySite(pai).getArgumentOperands()) {
1704+
if (valueMap.getTrackableValue(paiOp.get()).getRepresentative() ==
1705+
isolatedValue) {
1706+
// isolated_any causes all partial apply parameters to be shifted by 1
1707+
// due to the implicit isolated any parameter.
1708+
unsigned isIsolatedAny = pai->getFunctionType()->getIsolation() ==
1709+
SILFunctionTypeIsolation::Erased;
1710+
return ApplySite(pai).getAppliedArgIndex(paiOp) - isIsolatedAny;
1711+
}
1712+
}
1713+
1714+
return {};
1715+
}
15491716
};
15501717

15511718
} // namespace
15521719

1720+
bool TransferNonTransferrableDiagnosticInferrer::initForSendingPartialApply(
1721+
FullApplySite fas, Operand *paiOp) {
1722+
auto *pai =
1723+
dyn_cast<PartialApplyInst>(stripFunctionConversions(paiOp->get()));
1724+
if (!pai)
1725+
return false;
1726+
1727+
// For now we want this to be really narrow and to only apply to closure
1728+
// literals.
1729+
auto *ce = pai->getLoc().getAsASTNode<ClosureExpr>();
1730+
if (!ce)
1731+
return false;
1732+
1733+
// Ok, we now know we have a partial apply and it is a closure literal. Lets
1734+
// see if we can find the exact thing that caused the closure literal to be
1735+
// actor isolated.
1736+
auto isolationInfo = diagnosticEmitter.getIsolationRegionInfo();
1737+
if (isolationInfo->hasIsolatedValue()) {
1738+
// Now that we have the value, see if that value is one of our captured
1739+
// values.
1740+
auto isolatedValue = isolationInfo->getIsolatedValue();
1741+
auto matchingElt = getIsolatedValuePartialApplyIndex(pai, isolatedValue);
1742+
if (matchingElt) {
1743+
// Ok, we found the matching element. Lets emit our diagnostic!
1744+
auto capture = ce->getCaptureInfo().getCaptures()[*matchingElt];
1745+
diagnosticEmitter
1746+
.emitTypedSendingNeverSendableToSendingClosureParamDirectlyIsolated(
1747+
ce, capture);
1748+
return true;
1749+
}
1750+
}
1751+
1752+
// Ok, we are not tracking an actual isolated value or we do not capture the
1753+
// isolated value directly... we need to be smarter here. First lets gather up
1754+
// all non-Sendable values captured by the closure.
1755+
SmallVector<CapturedValue, 8> nonSendableCaptures;
1756+
for (auto capture : ce->getCaptureInfo().getCaptures()) {
1757+
auto *decl = capture.getDecl();
1758+
auto type = decl->getInterfaceType()->getCanonicalType();
1759+
auto silType = SILType::getPrimitiveObjectType(type);
1760+
if (!SILIsolationInfo::isNonSendableType(silType, pai->getFunction()))
1761+
continue;
1762+
1763+
auto *fromDC = decl->getInnermostDeclContext();
1764+
auto *nom = silType.getNominalOrBoundGenericNominal();
1765+
if (nom && fromDC) {
1766+
if (auto diagnosticBehavior =
1767+
getConcurrencyDiagnosticBehaviorLimit(nom, fromDC)) {
1768+
if (*diagnosticBehavior == DiagnosticBehavior::Ignore)
1769+
continue;
1770+
}
1771+
}
1772+
nonSendableCaptures.push_back(capture);
1773+
}
1774+
1775+
// If we do not have any non-Sendable captures... bail.
1776+
if (nonSendableCaptures.empty())
1777+
return false;
1778+
1779+
// Otherwise, emit the diagnostic.
1780+
diagnosticEmitter.emitTypedSendingNeverSendableToSendingClosureParam(
1781+
ce, nonSendableCaptures);
1782+
return true;
1783+
}
1784+
15531785
bool TransferNonTransferrableDiagnosticInferrer::initForIsolatedPartialApply(
15541786
Operand *op, AbstractClosureExpr *ace,
15551787
std::optional<ActorIsolation> actualCallerIsolation) {
@@ -1657,6 +1889,11 @@ bool TransferNonTransferrableDiagnosticInferrer::run() {
16571889
if (auto fas = FullApplySite::isa(op->getUser())) {
16581890
if (fas.getArgumentParameterInfo(*op).hasOption(
16591891
SILParameterInfo::Sending)) {
1892+
// Before we do anything, lets see if we are passing a sendable closure
1893+
// literal. If we do, we want to emit a special error that states which
1894+
// captured value caused the actual error.
1895+
if (initForSendingPartialApply(fas, op))
1896+
return true;
16601897

16611898
// See if we can infer a name from the value.
16621899
SmallString<64> resultingName;
@@ -1671,6 +1908,7 @@ bool TransferNonTransferrableDiagnosticInferrer::run() {
16711908
inferArgumentExprFromApplyExpr(sourceApply, fas, op)) {
16721909
type = inferredArgExpr->findOriginalType();
16731910
}
1911+
16741912
diagnosticEmitter.emitTypedSendingNeverSendableToSendingParam(loc,
16751913
type);
16761914
return true;
@@ -1796,7 +2034,8 @@ void TransferNonSendableImpl::emitTransferredNonTransferrableDiagnostics() {
17962034
llvm::dbgs() << "Emitting transfer non transferrable diagnostics.\n");
17972035

17982036
for (auto info : transferredNonTransferrableInfoList) {
1799-
TransferNonTransferrableDiagnosticInferrer diagnosticInferrer(info);
2037+
TransferNonTransferrableDiagnosticInferrer diagnosticInferrer(
2038+
regionInfo->getValueMap(), info);
18002039
diagnosticInferrer.run();
18012040
}
18022041
}

test/Concurrency/concurrent_value_checking.swift

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

415415
func test() {
416-
Task { // expected-tns-warning {{sending value of non-Sendable type '() async -> ()' risks causing data races}}
417-
// expected-tns-note @-1 {{Passing task-isolated value of non-Sendable type '() async -> ()' as a 'sending' parameter risks causing races inbetween task-isolated uses and uses reachable from the callee}}
418-
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}}
419418
}
420419

421-
Task { // expected-tns-warning {{sending value of non-Sendable type '() async -> ()' risks causing data races}}
422-
// expected-tns-note @-1 {{Passing task-isolated value of non-Sendable type '() async -> ()' as a 'sending' parameter risks causing races inbetween task-isolated uses and uses reachable from the callee}}
423-
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}}
424422
}
425423

426-
Task { [self] in // expected-tns-warning {{sending value of non-Sendable type '() async -> ()' risks causing data races}}
427-
// expected-tns-note @-1 {{Passing task-isolated value of non-Sendable type '() async -> ()' as a 'sending' parameter risks causing races inbetween task-isolated uses and uses reachable from the callee}}
428-
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}}
429426
}
430427

431-
Task { [self] in // expected-tns-warning {{sending value of non-Sendable type '() async -> ()' risks causing data races}}
432-
// expected-tns-note @-1 {{Passing task-isolated value of non-Sendable type '() async -> ()' as a 'sending' parameter risks causing races inbetween task-isolated uses and uses reachable from the callee}}
433-
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}}
434430
}
435431
}
436432
}

test/Concurrency/isolated_parameters.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -501,10 +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 {{sending value of non-Sendable type '() async -> ()' risks causing data races}}
505-
// expected-tns-note @-1 {{Passing 'a'-isolated value of non-Sendable type '() async -> ()' as a 'sending' parameter risks causing races inbetween 'a'-isolated uses and uses reachable from the callee}}
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}}
506505
_ = a
507-
_ = ns
506+
_ = ns // expected-tns-note {{closure captures 'a'-isolated 'ns'}}
508507
}
509508
}
510509

0 commit comments

Comments
 (0)