Skip to content

Commit a554ad6

Browse files
committed
[Concurrency] Diagnose mutating accesses to locals from concurrent code.
Replace the existing warning about any access to a local variable from concurrently-executing code with a more tailored error: concurrently-executing code may read a mutable varable, but cannot modify it. This is safe so long as we either always do by-value captures in concurrent closures or we ensure that no mutation of that variable can occur after the point of capture. We'll follow up with one of those. For now... be careful out there. Since we're promoting this to an error, narrow it down to concurrent closures and local functions, dropping the assumption that escaping closures "may execute concurrently."
1 parent 99f8d7a commit a554ad6

File tree

6 files changed

+155
-36
lines changed

6 files changed

+155
-36
lines changed

include/swift/AST/AnyFunctionRef.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,17 @@ class AnyFunctionRef {
181181
return false;
182182
}
183183

184+
/// Whether this function is @concurrent.
185+
bool isConcurrent() const {
186+
if (!hasType())
187+
return false;
188+
189+
if (auto *fnType = getType()->getAs<AnyFunctionType>())
190+
return fnType->isConcurrent();
191+
192+
return false;
193+
}
194+
184195
bool isObjC() const {
185196
if (auto afd = TheFunction.dyn_cast<AbstractFunctionDecl *>()) {
186197
return afd->isObjC();

include/swift/AST/DiagnosticsSema.def

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4253,9 +4253,8 @@ ERROR(global_actor_from_nonactor_context,none,
42534253
ERROR(actor_isolated_partial_apply,none,
42544254
"actor-isolated %0 %1 can not be partially applied",
42554255
(DescriptiveDeclKind, DeclName))
4256-
WARNING(concurrent_access_local,none,
4257-
"local %0 %1 is unsafe to reference in code that may execute "
4258-
"concurrently",
4256+
ERROR(concurrent_access_local,none,
4257+
"use of local %0 %1 in concurrently-executing code",
42594258
(DescriptiveDeclKind, DeclName))
42604259
ERROR(actor_isolated_from_concurrent_closure,none,
42614260
"actor-isolated %0 %1 cannot be referenced from a concurrent closure",
@@ -4272,6 +4271,9 @@ ERROR(actor_isolated_from_escaping_closure,none,
42724271
ERROR(local_function_executed_concurrently,none,
42734272
"concurrently-executed %0 %1 must be marked as '@concurrent'",
42744273
(DescriptiveDeclKind, DeclName))
4274+
ERROR(concurrent_mutation_of_local_capture,none,
4275+
"mutation of captured %0 %1 in concurrently-executing code",
4276+
(DescriptiveDeclKind, DeclName))
42754277
NOTE(concurrent_access_here,none,
42764278
"access in concurrently-executed code here", ())
42774279
NOTE(actor_isolated_sync_func,none,

lib/Sema/TypeCheckConcurrency.cpp

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,12 @@ namespace {
697697

698698
ConcurrentExecutionChecker concurrentExecutionChecker;
699699

700+
using MutableVarParent = llvm::PointerUnion<InOutExpr *, LoadExpr *>;
701+
702+
/// Mapping from mutable local variables to the parent expression, when
703+
/// that parent is either a load or a inout expression.
704+
llvm::SmallDenseMap<DeclRefExpr *, MutableVarParent, 4> mutableLocalVarParent;
705+
700706
const DeclContext *getDeclContext() const {
701707
return contextStack.back();
702708
}
@@ -709,6 +715,33 @@ namespace {
709715
useContext, defContext);
710716
}
711717

718+
/// If the subexpression is a reference to a mutable local variable from a
719+
/// different context, record its parent. We'll query this as part of
720+
/// capture semantics in concurrent functions.
721+
void recordMutableVarParent(MutableVarParent parent, Expr *subExpr) {
722+
auto declRef = dyn_cast<DeclRefExpr>(subExpr);
723+
if (!declRef)
724+
return;
725+
726+
auto var = dyn_cast_or_null<VarDecl>(declRef->getDecl());
727+
if (!var)
728+
return;
729+
730+
// Only mutable variables matter.
731+
if (!var->supportsMutation())
732+
return;
733+
734+
// Only mutable variables outside of the current context. This is an
735+
// optimization, because the parent map won't be queried in this case, and
736+
// it is the most common case for variables to be referenced in their
737+
// own context.
738+
if (var->getDeclContext() == getDeclContext())
739+
return;
740+
741+
assert(mutableLocalVarParent[declRef].isNull());
742+
mutableLocalVarParent[declRef] = parent;
743+
}
744+
712745
public:
713746
ActorIsolationChecker(const DeclContext *dc) : ctx(dc->getASTContext()) {
714747
contextStack.push_back(dc);
@@ -777,6 +810,12 @@ namespace {
777810
if (auto inout = dyn_cast<InOutExpr>(expr)) {
778811
if (!applyStack.empty())
779812
diagnoseInOutArg(applyStack.back(), inout, false);
813+
814+
recordMutableVarParent(inout, inout->getSubExpr());
815+
}
816+
817+
if (auto load = dyn_cast<LoadExpr>(expr)) {
818+
recordMutableVarParent(load, load->getSubExpr());
780819
}
781820

782821
if (auto lookup = dyn_cast<LookupExpr>(expr)) {
@@ -786,7 +825,8 @@ namespace {
786825
}
787826

788827
if (auto declRef = dyn_cast<DeclRefExpr>(expr)) {
789-
checkNonMemberReference(declRef->getDeclRef(), declRef->getLoc());
828+
checkNonMemberReference(
829+
declRef->getDeclRef(), declRef->getLoc(), declRef);
790830
return { true, expr };
791831
}
792832

@@ -865,6 +905,11 @@ namespace {
865905
applyStack.pop_back();
866906
}
867907

908+
// Clear out the mutable local variable parent map on the way out.
909+
if (auto *declRefExpr = dyn_cast<DeclRefExpr>(expr)) {
910+
mutableLocalVarParent.erase(declRefExpr);
911+
}
912+
868913
return expr;
869914
}
870915

@@ -1215,7 +1260,8 @@ namespace {
12151260
}
12161261

12171262
/// Check a reference to a local or global.
1218-
bool checkNonMemberReference(ConcreteDeclRef valueRef, SourceLoc loc) {
1263+
bool checkNonMemberReference(
1264+
ConcreteDeclRef valueRef, SourceLoc loc, DeclRefExpr *declRefExpr) {
12191265
if (!valueRef)
12201266
return false;
12211267

@@ -1233,22 +1279,37 @@ namespace {
12331279
value, loc, isolation.getGlobalActor());
12341280

12351281
case ActorIsolationRestriction::LocalCapture:
1236-
if (!shouldDiagnoseExistingDataRaces(getDeclContext()))
1282+
// Check whether we are in a context that will not execute concurrently
1283+
// with the context of 'self'. If not, it's safe.
1284+
if (!mayExecuteConcurrentlyWith(
1285+
getDeclContext(), isolation.getLocalContext()))
12371286
return false;
12381287

1239-
// Check whether we are in a context that will not execute concurrently
1240-
// with the context of 'self'.
1241-
if (mayExecuteConcurrentlyWith(
1242-
getDeclContext(), isolation.getLocalContext())) {
1288+
// Check whether this is a local variable, in which case we can
1289+
// determine whether it was captured by value.
1290+
if (auto var = dyn_cast<VarDecl>(value)) {
1291+
auto parent = mutableLocalVarParent[declRefExpr];
1292+
1293+
// If we have an immediate load of this variable, the by-value
1294+
// capture in a concurrent function will guarantee that this is
1295+
// safe.
1296+
if (parent.dyn_cast<LoadExpr *>())
1297+
return false;
1298+
1299+
// Otherwise, we have concurrent mutation. Complain.
12431300
ctx.Diags.diagnose(
1244-
loc, diag::concurrent_access_local,
1245-
value->getDescriptiveKind(), value->getName());
1246-
value->diagnose(
1247-
diag::kind_declared_here, value->getDescriptiveKind());
1301+
loc, diag::concurrent_mutation_of_local_capture,
1302+
var->getDescriptiveKind(), var->getName());
12481303
return true;
12491304
}
12501305

1251-
return false;
1306+
// Concurrent access to some other local.
1307+
ctx.Diags.diagnose(
1308+
loc, diag::concurrent_access_local,
1309+
value->getDescriptiveKind(), value->getName());
1310+
value->diagnose(
1311+
diag::kind_declared_here, value->getDescriptiveKind());
1312+
return true;
12521313

12531314
case ActorIsolationRestriction::Unsafe:
12541315
return diagnoseReferenceToUnsafeGlobal(value, loc);
@@ -1535,6 +1596,10 @@ SourceLoc ConcurrentExecutionChecker::getConcurrentReferenceLoc(
15351596
enclosingBody = enclosingFunc->getBody();
15361597
else if (auto enclosingClosure = dyn_cast<ClosureExpr>(enclosingDC))
15371598
enclosingBody = enclosingClosure->getBody();
1599+
else if (auto enclosingTopLevelCode = dyn_cast<TopLevelCodeDecl>(enclosingDC))
1600+
enclosingBody = enclosingTopLevelCode->getBody();
1601+
else
1602+
return SourceLoc();
15381603

15391604
assert(enclosingBody && "Cannot have a local function here");
15401605
ConcurrentLocalRefWalker walker(*this, localFunc);
@@ -1548,10 +1613,8 @@ bool ConcurrentExecutionChecker::mayExecuteConcurrentlyWith(
15481613
// Walk the context chain from the use to the definition.
15491614
while (useContext != defContext) {
15501615
// If we find a concurrent closure... it can be run concurrently.
1551-
// NOTE: We also classify escaping closures this way, which detects more
1552-
// problematic cases.
15531616
if (auto closure = dyn_cast<AbstractClosureExpr>(useContext)) {
1554-
if (isEscapingClosure(closure) || isConcurrentClosure(closure))
1617+
if (isConcurrentClosure(closure))
15551618
return true;
15561619
}
15571620

test/Concurrency/actor_isolation.swift

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ extension MyActor {
128128

129129
// Closures.
130130
let localConstant = 17
131-
var localVar = 17 // expected-note 4{{var declared here}}
131+
var localVar = 17
132132

133133
// Non-escaping closures are okay.
134134
acceptClosure {
@@ -142,31 +142,34 @@ extension MyActor {
142142
acceptConcurrentClosure {
143143
_ = self.text[0] // expected-error{{actor-isolated property 'text' cannot be referenced from a concurrent closure}}
144144
_ = self.synchronous() // expected-error{{actor-isolated instance method 'synchronous()' cannot be referenced from a concurrent closure}}
145-
_ = localVar // expected-warning{{local var 'localVar' is unsafe to reference in code that may execute concurrently}}
145+
_ = localVar // okay
146+
localVar = 25 // expected-error{{mutation of captured var 'localVar' in concurrently-executing code}}
146147
_ = localConstant
147148
}
148149

149150
// Escaping closures might run concurrently.
150151
acceptEscapingClosure {
151152
_ = self.text[0] // expected-error{{actor-isolated property 'text' cannot be referenced from an '@escaping' closure}}
152153
_ = self.synchronous() // expected-error{{actor-isolated instance method 'synchronous()' cannot be referenced from an '@escaping' closure}}
153-
_ = localVar // expected-warning{{local var 'localVar' is unsafe to reference in code that may execute concurrently}}
154+
_ = localVar // okay, don't complain about escaping
154155
_ = localConstant
155156
}
156157

157158
// Local functions might run concurrently.
158159
@concurrent func localFn1() {
159160
_ = self.text[0] // expected-error{{actor-isolated property 'text' cannot be referenced from a concurrent function}}
160161
_ = self.synchronous() // expected-error{{actor-isolated instance method 'synchronous()' cannot be referenced from a concurrent function}}
161-
_ = localVar // expected-warning{{local var 'localVar' is unsafe to reference in code that may execute concurrently}}
162+
_ = localVar // okay
163+
localVar = 25 // expected-error{{mutation of captured var 'localVar' in concurrently-executing code}}
162164
_ = localConstant
163165
}
164166

165167
@concurrent func localFn2() {
166168
acceptClosure {
167169
_ = text[0] // expected-error{{actor-isolated property 'text' cannot be referenced from a concurrent function}}
168170
_ = self.synchronous() // expected-error{{actor-isolated instance method 'synchronous()' cannot be referenced from a concurrent function}}
169-
_ = localVar // expected-warning{{local var 'localVar' is unsafe to reference in code that may execute concurrently}}
171+
_ = localVar // okay
172+
localVar = 25 // expected-error{{mutation of captured var 'localVar' in concurrently-executing code}}
170173
_ = localConstant
171174
}
172175
}
@@ -321,11 +324,12 @@ func testGlobalRestrictions(actor: MyActor) async {
321324
// Global mutable state cannot be accessed.
322325
_ = mutableGlobal // expected-warning{{reference to var 'mutableGlobal' is not concurrency-safe because it involves shared mutable state}}
323326

324-
// Local mutable variables cannot be accessed from concurrently-executing
327+
// Local mutable variables cannot be modified from concurrently-executing
325328
// code.
326-
var i = 17 // expected-note{{var declared here}}
327-
acceptEscapingClosure {
328-
i = 42 // expected-warning{{local var 'i' is unsafe to reference in code that may execute concurrently}}
329+
var i = 17
330+
acceptConcurrentClosure {
331+
_ = i
332+
i = 42 // expected-error{{mutation of captured var 'i' in concurrently-executing code}}
329333
}
330334
print(i)
331335
}
@@ -335,15 +339,14 @@ func testGlobalRestrictions(actor: MyActor) async {
335339
// ----------------------------------------------------------------------
336340
func checkLocalFunctions() async {
337341
var i = 0
338-
var j = 0 // expected-note{{var declared here}}
342+
var j = 0
339343

340344
func local1() {
341345
i = 17
342346
}
343347

344348
func local2() { // expected-error{{concurrently-executed local function 'local2()' must be marked as '@concurrent'}}{{3-3=@concurrent }}
345-
j = 42 // expected-warning{{local var 'j' is unsafe to reference in code that may execute concurrently}}
346-
// FIXME: the above should be an error as well
349+
j = 42 // expected-error{{mutation of captured var 'j' in concurrently-executing code}}
347350
}
348351

349352
// Okay to call locally.
@@ -357,22 +360,22 @@ func checkLocalFunctions() async {
357360
}
358361

359362
// Escaping closures can make the local function execute concurrently.
360-
acceptEscapingClosure {
363+
acceptConcurrentClosure {
361364
local2() // expected-note{{access in concurrently-executed code here}}
362365
}
363366

364367
print(i)
365368
print(j)
366369

367-
var k = 17 // expected-note{{var declared here}}
370+
var k = 17
368371
func local4() {
369-
acceptEscapingClosure {
372+
acceptConcurrentClosure {
370373
local3() // expected-note{{access in concurrently-executed code here}}
371374
}
372375
}
373376

374377
func local3() { // expected-error{{concurrently-executed local function 'local3()' must be marked as '@concurrent'}}
375-
k = 25 // expected-warning{{local var 'k' is unsafe to reference in code that may execute concurrently}}
378+
k = 25 // expected-error{{mutation of captured var 'k' in concurrently-executing code}}
376379
}
377380

378381
print(k)

test/Concurrency/async_let_isolation.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ actor class MyActor {
1717
async let z = synchronous()
1818
// expected-error @-1{{actor-isolated instance method 'synchronous()' cannot be referenced from 'async let' initializer}}
1919

20-
var localText = text // expected-note{{var declared here}}
20+
var localText = text
2121
async let w = localText.removeLast()
22-
// expected-warning@-1{{local var 'localText' is unsafe to reference in code that may execute concurrently}}
22+
// expected-error@-1{{mutation of captured var 'localText' in concurrently-executing code}}
2323

2424
_ = await x
2525
_ = await y

test/attr/attr_concurrent.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,43 @@ func closures() {
5353
acceptsConcurrent(closure1) // expected-error{{converting non-concurrent function value to '@concurrent (Int) -> Int' may introduce data races}}
5454
}
5555

56+
// Mutation of captured locals from within @concurrent functions.
57+
extension Int {
58+
mutating func makeNegative() {
59+
self = -self
60+
}
61+
62+
func printMe() {
63+
print(self)
64+
}
65+
}
66+
67+
func mutationOfLocal() {
68+
var localInt = 17
69+
acceptsConcurrent { i in
70+
// Non-mutating accesses are okay
71+
print(localInt + 17)
72+
localInt.printMe()
73+
74+
// Mutations of locally-defined variables are fine.
75+
var localResult = localInt + 1
76+
print(localResult)
77+
78+
_ = {
79+
localResult = localResult + 1
80+
}()
81+
82+
// Mutations of captured variables executing concurrently are bad.
83+
localInt = 17 // expected-error{{mutation of captured var 'localInt' in concurrently-executing code}}
84+
localInt += 1 // expected-error{{mutation of captured var 'localInt' in concurrently-executing code}}
85+
localInt.makeNegative() // expected-error{{mutation of captured var 'localInt' in concurrently-executing code}}
86+
87+
_ = {
88+
localInt = localInt + 12 // expected-error{{mutation of captured var 'localInt' in concurrently-executing code}}
89+
}()
90+
91+
return i + localInt
92+
}
93+
94+
localInt = 20
95+
}

0 commit comments

Comments
 (0)