Skip to content

Commit f3da35a

Browse files
authored
Merge pull request swiftlang#34688 from etcwilde/ewilde/disallow-passing-actor-state-inout
[Concurrency] Ban passing actor state via inout Resolves: rdar://70635479
2 parents fdcb9f8 + 66d307e commit f3da35a

File tree

3 files changed

+231
-2
lines changed

3 files changed

+231
-2
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4210,6 +4210,13 @@ ERROR(actor_isolated_self_independent_context,none,
42104210
"actor-isolated %0 %1 can not be referenced from an "
42114211
"'@actorIndependent' context",
42124212
(DescriptiveDeclKind, DeclName))
4213+
ERROR(actor_isolated_inout_state,none,
4214+
"actor-isolated %0 %1 cannot be passed 'inout' to"
4215+
"%select{| implicitly}2 'async' function call",
4216+
(DescriptiveDeclKind, DeclName, bool))
4217+
ERROR(actor_isolated_mutating_func,none,
4218+
"cannot call mutating async function %0 on actor-isolated %1 %2",
4219+
(DeclName, DescriptiveDeclKind, DeclName))
42134220
ERROR(actor_isolated_global_actor_context,none,
42144221
"actor-isolated %0 %1 can not be referenced from context of global "
42154222
"actor %2",

lib/Sema/TypeCheckConcurrency.cpp

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,24 @@ findMemberReference(Expr *expr) {
611611
return None;
612612
}
613613

614+
/// Return true if the callee of an ApplyExpr is async
615+
///
616+
/// Note that this must be called after the implicitlyAsync flag has been set,
617+
/// or implicitly async calls will not return the correct value.
618+
static bool isAsyncCall(const ApplyExpr *call) {
619+
if (call->implicitlyAsync())
620+
return true;
621+
622+
// Effectively the same as doing a
623+
// `cast_or_null<FunctionType>(call->getFn()->getType())`, check the
624+
// result of that and then checking `isAsync` if it's defined.
625+
Type funcTypeType = call->getFn()->getType();
626+
if (!funcTypeType)
627+
return false;
628+
FunctionType *funcType = funcTypeType->castTo<FunctionType>();
629+
return funcType->isAsync();
630+
}
631+
614632
namespace {
615633
/// Check for adherence to the actor isolation rules, emitting errors
616634
/// when actor-isolated declarations are used in an unsafe manner.
@@ -679,6 +697,11 @@ namespace {
679697
return { true, expr };
680698
}
681699

700+
if (auto inout = dyn_cast<InOutExpr>(expr)) {
701+
if (!applyStack.empty())
702+
diagnoseInOutArg(applyStack.back(), inout, false);
703+
}
704+
682705
if (auto lookup = dyn_cast<LookupExpr>(expr)) {
683706
checkMemberReference(lookup->getBase(), lookup->getMember(),
684707
lookup->getLoc());
@@ -720,11 +743,22 @@ namespace {
720743
Expr *fn = call->getFn()->getValueProvidingExpr();
721744
if (auto memberRef = findMemberReference(fn)) {
722745
checkMemberReference(
723-
call->getArg(), memberRef->first, memberRef->second,
746+
call->getArg(), memberRef->first, memberRef->second,
724747
/*isEscapingPartialApply=*/false, /*maybeImplicitAsync=*/true);
725748

726749
call->getArg()->walk(*this);
727750

751+
if (applyStack.size() >= 2) {
752+
ApplyExpr *outerCall = applyStack[applyStack.size() - 2];
753+
if (isAsyncCall(outerCall)) {
754+
// This call is a partial application within an async call.
755+
// If the partial application take a value inout, it is bad.
756+
if (InOutExpr *inoutArg = dyn_cast<InOutExpr>(
757+
call->getArg()->getSemanticsProvidingExpr()))
758+
diagnoseInOutArg(outerCall, inoutArg, true);
759+
}
760+
}
761+
728762
// manual clean-up since normal traversal is skipped
729763
assert(applyStack.back() == dyn_cast<ApplyExpr>(expr));
730764
applyStack.pop_back();
@@ -860,6 +894,59 @@ namespace {
860894
return true;
861895
}
862896

897+
/// Diagnose an inout argument passed into an async call
898+
///
899+
/// \returns true if we diagnosed the entity, \c false otherwise.
900+
bool diagnoseInOutArg(const ApplyExpr *call, const InOutExpr *arg,
901+
bool isPartialApply) {
902+
// check that the call is actually async
903+
if (!isAsyncCall(call))
904+
return false;
905+
906+
Expr *subArg = arg->getSubExpr();
907+
if (LookupExpr *baseArg = dyn_cast<LookupExpr>(subArg)) {
908+
while (LookupExpr *nextLayer = dyn_cast<LookupExpr>(baseArg->getBase()))
909+
baseArg = nextLayer;
910+
// subArg: the actual property being passed inout
911+
// baseArg: the property in the actor who's property is being passed
912+
// inout
913+
914+
auto memberDecl = baseArg->getMember().getDecl();
915+
auto isolation = ActorIsolationRestriction::forDeclaration(memberDecl);
916+
switch (isolation) {
917+
case ActorIsolationRestriction::Unrestricted:
918+
case ActorIsolationRestriction::LocalCapture:
919+
case ActorIsolationRestriction::Unsafe:
920+
case ActorIsolationRestriction::GlobalActor: // TODO: handle global
921+
// actors
922+
break;
923+
case ActorIsolationRestriction::ActorSelf: {
924+
if (isPartialApply) {
925+
// The partially applied InoutArg is a property of actor. This can
926+
// really only happen when the property is a struct with a mutating
927+
// async method.
928+
if (auto partialApply = dyn_cast<ApplyExpr>(call->getFn())) {
929+
ValueDecl *fnDecl =
930+
cast<DeclRefExpr>(partialApply->getFn())->getDecl();
931+
ctx.Diags.diagnose(
932+
call->getLoc(), diag::actor_isolated_mutating_func,
933+
fnDecl->getName(), memberDecl->getDescriptiveKind(),
934+
memberDecl->getName());
935+
return true;
936+
}
937+
} else {
938+
ctx.Diags.diagnose(
939+
subArg->getLoc(), diag::actor_isolated_inout_state,
940+
memberDecl->getDescriptiveKind(), memberDecl->getName(),
941+
call->implicitlyAsync());
942+
return true;
943+
}
944+
}
945+
}
946+
}
947+
return false;
948+
}
949+
863950
/// Get the actor isolation of the innermost relevant context.
864951
ActorIsolation getInnermostIsolatedContext(const DeclContext *constDC) {
865952
// Retrieve the actor isolation for a declaration somewhere in our
@@ -1196,7 +1283,9 @@ namespace {
11961283
llvm_unreachable("Locals cannot be referenced with member syntax");
11971284

11981285
case ActorIsolationRestriction::Unsafe:
1199-
return diagnoseReferenceToUnsafe(member, memberLoc);
1286+
// This case is hit when passing actor state inout to functions in some
1287+
// cases. The error is emitted by diagnoseInOutArg.
1288+
return false;
12001289
}
12011290
llvm_unreachable("unhandled actor isolation kind!");
12021291
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// RUN: %target-typecheck-verify-swift -enable-experimental-concurrency
2+
// REQUIRES: concurrency
3+
4+
// Verify that we don't allow actor-isolated state to be passed via inout
5+
// Check:
6+
// - can't pass it into a normal async function
7+
// - can't pass it into a first-class async function as a value
8+
// - can't pass it into another actor method
9+
// - can't pass it into a curried/partially applied function
10+
// - can't pass it inout to a function that doesn't directly touch it
11+
// - can't pass it into a function that was passed into the calling method
12+
// - can't call async mutating function on actor isolated state
13+
14+
struct Point {
15+
var x: Int
16+
var y: Int
17+
18+
mutating func setComponents(x: inout Int, y: inout Int) async {
19+
defer { (x, y) = (self.x, self.y) }
20+
(self.x, self.y) = (x, y)
21+
}
22+
}
23+
24+
actor class TestActor {
25+
var position = Point(x: 0, y: 0)
26+
var nextPosition = Point(x: 0, y: 1)
27+
var value1: Int = 0
28+
var value2: Int = 1
29+
}
30+
31+
func modifyAsynchronously(_ foo: inout Int) async { foo += 1 }
32+
let modifyAsyncValue = modifyAsynchronously
33+
34+
// external function call
35+
extension TestActor {
36+
37+
// Can't pass actor-isolated primitive into a function
38+
func inoutAsyncFunctionCall() async {
39+
// expected-error@+1{{actor-isolated property 'value1' cannot be passed 'inout' to 'async' function call}}
40+
await modifyAsynchronously(&value1)
41+
}
42+
43+
func inoutAsyncClosureCall() async {
44+
// expected-error@+1{{actor-isolated property 'value1' cannot be passed 'inout' to 'async' function call}}
45+
await { (_ foo: inout Int) async in foo += 1 }(&value1)
46+
}
47+
48+
// Can't pass actor-isolated primitive into first-class function value
49+
func inoutAsyncValueCall() async {
50+
// expected-error@+1{{actor-isolated property 'value1' cannot be passed 'inout' to 'async' function call}}
51+
await modifyAsyncValue(&value1)
52+
}
53+
54+
// Can't pass property of actor-isolated state inout to async function
55+
func inoutPropertyStateValueCall() async {
56+
// expected-error@+1{{actor-isolated property 'position' cannot be passed 'inout' to 'async' function call}}
57+
await modifyAsynchronously(&position.x)
58+
}
59+
}
60+
61+
// internal method call
62+
extension TestActor {
63+
func modifyByValue(_ other: inout Int) async {
64+
other += value1
65+
}
66+
67+
func passStateIntoMethod() async {
68+
// expected-error@+1{{actor-isolated property 'value1' cannot be passed 'inout' to 'async' function call}}
69+
await modifyByValue(&value1)
70+
}
71+
}
72+
73+
// external class method call
74+
class NonAsyncClass {
75+
func modifyOtherAsync(_ other : inout Int) async {
76+
// ...
77+
}
78+
79+
func modifyOtherNotAsync(_ other: inout Int) {
80+
// ...
81+
}
82+
}
83+
84+
// Calling external class/struct async function
85+
extension TestActor {
86+
// Can't pass state into async method of another class
87+
88+
func passStateIntoDifferentClassMethod() async {
89+
let other = NonAsyncClass()
90+
let otherCurry = other.modifyOtherAsync
91+
// expected-error@+1{{actor-isolated property 'value2' cannot be passed 'inout' to 'async' function call}}
92+
await other.modifyOtherAsync(&value2)
93+
// expected-error@+1{{actor-isolated property 'value1' cannot be passed 'inout' to 'async' function call}}
94+
await otherCurry(&value1)
95+
other.modifyOtherNotAsync(&value2) // This is okay since it's not async!
96+
97+
}
98+
99+
func callMutatingFunctionOnStruct() async {
100+
// expected-error@+3:20{{cannot call mutating async function 'setComponents(x:y:)' on actor-isolated property 'position'}}
101+
// expected-error@+2:51{{actor-isolated property 'nextPosition' cannot be passed 'inout' to 'async' function call}}
102+
// expected-error@+1:71{{actor-isolated property 'nextPosition' cannot be passed 'inout' to 'async' function call}}
103+
await position.setComponents(x: &nextPosition.x, y: &nextPosition.y)
104+
105+
// expected-error@+3:20{{cannot call mutating async function 'setComponents(x:y:)' on actor-isolated property 'position'}}
106+
// expected-error@+2:38{{actor-isolated property 'value1' cannot be passed 'inout' to 'async' function call}}
107+
// expected-error@+1:50{{actor-isolated property 'value2' cannot be passed 'inout' to 'async' function call}}
108+
await position.setComponents(x: &value1, y: &value2)
109+
}
110+
}
111+
112+
// Check implicit async testing
113+
actor class DifferentActor {
114+
func modify(_ state: inout Int) {}
115+
}
116+
117+
extension TestActor {
118+
func modify(_ state: inout Int) {}
119+
120+
// Actor state passed inout to implicitly async function on an actor of the
121+
// same type
122+
func modifiedByOtherTestActor(_ other: TestActor) async {
123+
//expected-error@+1{{actor-isolated property 'value2' cannot be passed 'inout' to implicitly 'async' function call}}
124+
await other.modify(&value2)
125+
}
126+
127+
// Actor state passed inout to an implicitly async function on an actor of a
128+
// different type
129+
func modifiedByOther(_ other: DifferentActor) async {
130+
//expected-error@+1{{actor-isolated property 'value2' cannot be passed 'inout' to implicitly 'async' function call}}
131+
await other.modify(&value2)
132+
}
133+
}

0 commit comments

Comments
 (0)