Skip to content

Commit a09224d

Browse files
committed
[Concurrency] Ban passing actor state via inout
Passing actor statte to async functions via inout parameters violates automicity. This patch bans passing actor state via inout parameter to async functions. This removes the warning when passing a sub-property of a property of an actor class and replaces it with an error message since passing actor state inout is not allowed.
1 parent 8dec996 commit a09224d

File tree

3 files changed

+206
-2
lines changed

3 files changed

+206
-2
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4206,6 +4206,12 @@ ERROR(actor_isolated_self_independent_context,none,
42064206
"actor-isolated %0 %1 can not be referenced from an "
42074207
"'@actorIndependent' context",
42084208
(DescriptiveDeclKind, DeclName))
4209+
ERROR(actor_isolated_inout_state,none,
4210+
"actor-isolated %0 %1 cannot be passed 'inout' to asynchronous function",
4211+
(DescriptiveDeclKind, DeclName))
4212+
ERROR(actor_isolated_mutating_func,none,
4213+
"cannot call mutating async function %0 on actor-isolated %1 %2",
4214+
(DeclName, DescriptiveDeclKind, DeclName))
42094215
ERROR(actor_isolated_global_actor_context,none,
42104216
"actor-isolated %0 %1 can not be referenced from context of global "
42114217
"actor %2",

lib/Sema/TypeCheckConcurrency.cpp

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,24 @@ findMemberReference(Expr *expr) {
604604
return None;
605605
}
606606

607+
/// Return true if the callee of an ApplyExpr is async
608+
///
609+
/// Note that this must be called after the implicitlyAsync flag has been set,
610+
/// or implicitly async calls will not return the correct value.
611+
static bool isAsyncCall(const ApplyExpr *call) {
612+
if (call->implicitlyAsync())
613+
return true;
614+
615+
// Effectively the same as doing a
616+
// `cast_or_null<FunctionType>(call->getFn()->getType())`, check the
617+
// result of that and then checking `isAsync` if it's defined.
618+
Type funcTypeType = call->getFn()->getType();
619+
if (!funcTypeType)
620+
return false;
621+
FunctionType *funcType = funcTypeType->castTo<FunctionType>();
622+
return funcType->isAsync();
623+
}
624+
607625
namespace {
608626
/// Check for adherence to the actor isolation rules, emitting errors
609627
/// when actor-isolated declarations are used in an unsafe manner.
@@ -672,6 +690,11 @@ namespace {
672690
return { true, expr };
673691
}
674692

693+
if (auto inout = dyn_cast<InOutExpr>(expr)) {
694+
if (!applyStack.empty())
695+
diagnoseInOutArg(applyStack.back(), inout, false);
696+
}
697+
675698
if (auto lookup = dyn_cast<LookupExpr>(expr)) {
676699
checkMemberReference(lookup->getBase(), lookup->getMember(),
677700
lookup->getLoc());
@@ -713,11 +736,22 @@ namespace {
713736
Expr *fn = call->getFn()->getValueProvidingExpr();
714737
if (auto memberRef = findMemberReference(fn)) {
715738
checkMemberReference(
716-
call->getArg(), memberRef->first, memberRef->second,
739+
call->getArg(), memberRef->first, memberRef->second,
717740
/*isEscapingPartialApply=*/false, /*maybeImplicitAsync=*/true);
718741

719742
call->getArg()->walk(*this);
720743

744+
if (applyStack.size() >= 2) {
745+
ApplyExpr *outerCall = applyStack[applyStack.size() - 2];
746+
if (isAsyncCall(outerCall)) {
747+
// This call is a partial application within an async call.
748+
// If the partial application take a value inout, it is bad.
749+
if (InOutExpr *inoutArg = dyn_cast<InOutExpr>(
750+
call->getArg()->getSemanticsProvidingExpr()))
751+
diagnoseInOutArg(outerCall, inoutArg, true);
752+
}
753+
}
754+
721755
// manual clean-up since normal traversal is skipped
722756
assert(applyStack.back() == dyn_cast<ApplyExpr>(expr));
723757
applyStack.pop_back();
@@ -853,6 +887,58 @@ namespace {
853887
return true;
854888
}
855889

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

11401226
case ActorIsolationRestriction::Unsafe:
1141-
return diagnoseReferenceToUnsafe(member, memberLoc);
1227+
// This case is hit when passing actor state inout to functions in some
1228+
// cases. The error is emitted by diagnoseInOutArg.
1229+
return false;
11421230
}
11431231
llvm_unreachable("unhandled actor isolation kind!");
11441232
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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 asynchronous function}}
40+
await modifyAsynchronously(&value1)
41+
}
42+
43+
func inoutAsyncClosureCall() async {
44+
// expected-error@+1{{actor-isolated property 'value1' cannot be passed 'inout' to asynchronous function}}
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 asynchronous function}}
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 asynchronous function}}
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 asynchronous function}}
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 asynchronous function}}
92+
await other.modifyOtherAsync(&value2)
93+
// expected-error@+1{{actor-isolated property 'value1' cannot be passed 'inout' to asynchronous function}}
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 asynchronous function}}
102+
// expected-error@+1:71{{actor-isolated property 'nextPosition' cannot be passed 'inout' to asynchronous function}}
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 asynchronous function}}
107+
// expected-error@+1:50{{actor-isolated property 'value2' cannot be passed 'inout' to asynchronous function}}
108+
await position.setComponents(x: &value1, y: &value2)
109+
}
110+
}

0 commit comments

Comments
 (0)