Skip to content

Commit e0673c9

Browse files
committed
[Concurrency] Suggest adding a method, when mutating actor property cross isolation
This comes as an idea from the [Why do not actor-isolated properties support 'await' setter](https://forums.swift.org/t/why-do-not-actor-isolated-properties-support-await-setter/73867/26) thread. The note that > Actor-isolated property 'colorFill' can not be mutated from the main actor Leads people to assume that this is somehow "broken" while they could add a metho to perform the mutation and move on. A developer comments that: > So I just assumed that this meant that it was fundamentally wrong to > mutate this actor's properties from another actor, while all I actually > needed was... a 3 line setter.
1 parent bafe819 commit e0673c9

11 files changed

+121
-50
lines changed

include/swift/AST/ActorIsolation.h

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,7 @@ class ActorIsolation {
9191

9292
ActorIsolation(Kind kind, Expr *actor, unsigned parameterIndex);
9393

94-
ActorIsolation(Kind kind, Type globalActor)
95-
: globalActor(globalActor), kind(kind), isolatedByPreconcurrency(false),
96-
silParsed(false), parameterIndex(0) {}
94+
ActorIsolation(Kind kind, Type globalActor);
9795

9896
public:
9997
// No-argument constructor needed for DenseMap use in PostfixCompletion.cpp
@@ -203,7 +201,7 @@ class ActorIsolation {
203201
}
204202

205203
NominalTypeDecl *getActor() const;
206-
NominalTypeDecl *getActorOrNullPtr() const;
204+
// NominalTypeDecl *getActorOrNullPtr() const;
207205

208206
VarDecl *getActorInstance() const;
209207

include/swift/AST/DiagnosticsSema.def

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5455,6 +5455,12 @@ ERROR(distributed_actor_isolated_non_self_reference,none,
54555455
"distributed actor-isolated %kind0 can not be accessed from a "
54565456
"nonisolated context",
54575457
(const ValueDecl *))
5458+
NOTE(note_consider_method_for_isolated_property_mutation,none,
5459+
"consider declaring an %0 %1 method to perform the mutation",
5460+
(const NominalTypeDecl *, ActorIsolation))
5461+
NOTE(note_consider_method_for_global_actor_isolated_property_mutation,none, // TODO: de-duplicate with note_consider_method_for_isolated_property_mutation
5462+
"consider declaring an %0 method to perform the mutation",
5463+
(ActorIsolation))
54585464
ERROR(conflicting_stored_property_isolation,none,
54595465
"%select{default|memberwise}0 initializer for %1 cannot be both %2 and %3",
54605466
(bool, Type, ActorIsolation, ActorIsolation))

lib/AST/Decl.cpp

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11651,6 +11651,10 @@ ActorIsolation::ActorIsolation(Kind kind, Expr *actor,
1165111651
: actorInstance(actor), kind(kind), isolatedByPreconcurrency(false),
1165211652
silParsed(false), parameterIndex(parameterIndex) {}
1165311653

11654+
ActorIsolation::ActorIsolation(Kind kind, Type globalActor)
11655+
: globalActor(globalActor), kind(kind), isolatedByPreconcurrency(false),
11656+
silParsed(false), parameterIndex(0) {}
11657+
1165411658
ActorIsolation
1165511659
ActorIsolation::forActorInstanceParameter(Expr *actor,
1165611660
unsigned parameterIndex) {
@@ -11701,51 +11705,29 @@ ActorIsolation ActorIsolation::forActorInstanceSelf(NominalTypeDecl *selfDecl) {
1170111705
}
1170211706

1170311707
NominalTypeDecl *ActorIsolation::getActor() const {
11704-
assert(getKind() == ActorInstance ||
11705-
getKind() == GlobalActor);
11708+
assert(getKind() == ActorInstance || getKind() == GlobalActor);
1170611709

1170711710
if (silParsed)
1170811711
return nullptr;
1170911712

11710-
Type actorType;
11711-
11712-
if (auto *instance = actorInstance.dyn_cast<VarDecl *>()) {
11713-
actorType = instance->getTypeInContext();
11714-
} else if (auto *instance = actorInstance.dyn_cast<Expr *>()) {
11715-
actorType = instance->getType();
11716-
}
11717-
11718-
if (actorType) {
11719-
if (auto wrapped = actorType->getOptionalObjectType()) {
11720-
actorType = wrapped;
11721-
}
11722-
return actorType
11723-
->getReferenceStorageReferent()->getAnyActor();
11713+
if (getKind() == GlobalActor) {
11714+
return getGlobalActor()->getAnyNominal();
1172411715
}
1172511716

11726-
return actorInstance.get<NominalTypeDecl *>();
11727-
}
11728-
11729-
NominalTypeDecl *ActorIsolation::getActorOrNullPtr() const {
11730-
if (getKind() != ActorInstance || getKind() != GlobalActor)
11731-
return nullptr;
11732-
11733-
if (silParsed)
11734-
return nullptr;
11735-
1173611717
Type actorType;
1173711718

1173811719
if (auto *instance = actorInstance.dyn_cast<VarDecl *>()) {
1173911720
actorType = instance->getTypeInContext();
11740-
} else if (auto *instance = actorInstance.dyn_cast<Expr *>()) {
11741-
actorType = instance->getType();
11721+
} else if (auto *expr = actorInstance.dyn_cast<Expr *>()) {
11722+
actorType = expr->getType();
1174211723
}
1174311724

1174411725
if (actorType) {
1174511726
if (auto wrapped = actorType->getOptionalObjectType()) {
1174611727
actorType = wrapped;
1174711728
}
11748-
return actorType->getReferenceStorageReferent()->getAnyActor();
11729+
return actorType
11730+
->getReferenceStorageReferent()->getAnyActor();
1174911731
}
1175011732

1175111733
return actorInstance.get<NominalTypeDecl *>();
@@ -11785,7 +11767,10 @@ bool ActorIsolation::isDistributedActor() const {
1178511767
if (silParsed)
1178611768
return false;
1178711769

11788-
return getKind() == ActorInstance && getActor()->isDistributedActor();
11770+
if (getKind() != ActorInstance)
11771+
return false;
11772+
11773+
return getActor()->isDistributedActor();
1178911774
}
1179011775

1179111776
bool ActorIsolation::isEqual(const ActorIsolation &lhs,

lib/Sema/TypeCheckConcurrency.cpp

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1823,6 +1823,52 @@ static bool wasLegacyEscapingUseRestriction(AbstractFunctionDecl *fn) {
18231823
return !(fn->hasAsync()); // basic case: not async = had restriction.
18241824
}
18251825

1826+
/// Note that while a direct access to the actor isolated property is not legal
1827+
/// you may want to consider introducing an accessing method for a mutation.
1828+
static void
1829+
maybeNoteMutatingMethodSuggestion(ASTContext &C,
1830+
ValueDecl const *member,
1831+
SourceLoc memberLoc,
1832+
DeclContext const *refCxt,
1833+
ActorIsolation isolation,
1834+
std::optional<VarRefUseEnv> useKind) {
1835+
if (!member || !isa<VarDecl>(member))
1836+
return; // we only offer the note property mutations
1837+
1838+
if (!(isolation.getKind() == ActorIsolation::Kind::ActorInstance ||
1839+
isolation.getKind() == ActorIsolation::Kind::GlobalActor)) {
1840+
return;
1841+
}
1842+
1843+
if (useKind != VarRefUseEnv::Mutating) {
1844+
// This note is tailored for the 'mutating' access, i.e. when
1845+
// attempting to mutate a property, they should instead make an actor method to perform the mutation. Reading properties does not have the same restriction.
1846+
return;
1847+
}
1848+
1849+
if (!refCxt->isAsyncContext()) {
1850+
// don't suggest creating method when in sync context, as we won't be able
1851+
// to invoke it anyway so this would not be helpful to suggest
1852+
return;
1853+
}
1854+
1855+
if (auto actor = isolation.getActor()) {
1856+
// TODO: isolation is printing without the type name for instance actors,
1857+
// it'd be nicer to print with type name and avoid this switch
1858+
if (isolation.getKind() == swift::ActorIsolation::ActorInstance) {
1859+
C.Diags.diagnose(
1860+
memberLoc,
1861+
diag::note_consider_method_for_isolated_property_mutation,
1862+
actor, isolation);
1863+
} else if (isolation.getKind() == swift::ActorIsolation::GlobalActor) {
1864+
C.Diags.diagnose(
1865+
memberLoc,
1866+
diag::note_consider_method_for_global_actor_isolated_property_mutation,
1867+
isolation);
1868+
}
1869+
}
1870+
}
1871+
18261872
/// Note that the given actor member is isolated.
18271873
static void noteIsolatedActorMember(ValueDecl const *decl,
18281874
std::optional<VarRefUseEnv> useKind) {
@@ -1944,20 +1990,23 @@ static bool memberAccessHasSpecialPermissionInSwift5(
19441990
}
19451991

19461992
// Otherwise, it's definitely going to be illegal, so warn and permit.
1947-
auto &diags = refCxt->getASTContext().Diags;
1993+
auto &C = refCxt->getASTContext();
1994+
auto &diags = C.Diags;
19481995
auto useKindInt = static_cast<unsigned>(
19491996
useKind.value_or(VarRefUseEnv::Read));
19501997

1998+
auto isolation = getActorIsolation(const_cast<ValueDecl *>(member));
19511999
diags.diagnose(
19522000
memberLoc, diag::actor_isolated_non_self_reference,
19532001
member,
19542002
useKindInt,
19552003
baseActor.kind + 1,
19562004
baseActor.globalActor,
1957-
getActorIsolation(const_cast<ValueDecl *>(member)))
2005+
isolation)
19582006
.warnUntilSwiftVersion(6);
19592007

19602008
noteIsolatedActorMember(member, useKind);
2009+
maybeNoteMutatingMethodSuggestion(C, member, memberLoc, refCxt, isolation, useKind);
19612010
return true;
19622011
}
19632012

@@ -4438,6 +4487,8 @@ namespace {
44384487
refKind + 1, refGlobalActor,
44394488
result.isolation)
44404489
.warnUntilSwiftVersionIf(preconcurrencyContext, 6);
4490+
maybeNoteMutatingMethodSuggestion(ctx, decl, loc, getDeclContext(), result.isolation,
4491+
kindOfUsage(decl, context).value_or(VarRefUseEnv::Read));
44414492

44424493
if (derivedConformanceType) {
44434494
auto *decl = dyn_cast<ValueDecl>(getDeclContext()->getAsDecl());

test/Concurrency/Inputs/GlobalVariables.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ public struct Globals {
22
public static let integerConstant = 0
33
public static var integerMutable = 0
44

5-
public static nonisolated(unsafe) let nonisolatedUnsafeIntegerConstant = 0
5+
public static let nonisolatedUnsafeIntegerConstant = 0
66
public static nonisolated(unsafe) var nonisolatedUnsafeIntegerMutable = 0
77

88
@MainActor public static var actorInteger = 0

test/Concurrency/actor_call_implicitly_async.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,9 @@ func anotherAsyncFunc() async {
232232

233233
_ = b.balance // expected-error {{actor-isolated instance method 'balance()' can not be partially applied}}
234234

235-
a.owner = "cat" // expected-error{{actor-isolated property 'owner' can not be mutated from a nonisolated context}}
235+
// expected-error@+2{{actor-isolated property 'owner' can not be mutated from a nonisolated context}}
236+
// expected-note@+1{{consider declaring an 'BankAccount' actor-isolated method to perform the mutation}}
237+
a.owner = "cat"
236238
// expected-error@+1{{expression is 'async' but is not marked with 'await'}} {{7-7=await }} expected-note@+1{{property access is 'async'}}
237239
_ = b.owner
238240
_ = await b.owner == "cat"
@@ -279,7 +281,9 @@ func blender(_ peeler : () -> Void) {
279281
var money = await dollarsInBananaStand
280282
money -= 1200
281283

282-
dollarsInBananaStand = money // expected-error{{global actor 'BananaActor'-isolated var 'dollarsInBananaStand' can not be mutated from global actor 'OrangeActor'}}
284+
// expected-error@+2{{global actor 'BananaActor'-isolated var 'dollarsInBananaStand' can not be mutated from global actor 'OrangeActor'}}
285+
// expected-note@+1{{consider declaring an global actor 'BananaActor'-isolated method to perform the mutation}}
286+
dollarsInBananaStand = money
283287

284288
// FIXME: these two errors seem a bit redundant.
285289
// expected-error@+2 {{actor-isolated var 'dollarsInBananaStand' cannot be passed 'inout' to implicitly 'async' function call}}

test/Concurrency/actor_existentials.swift

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,16 @@ func from_isolated_existential2(_ x: isolated any P) async {
3131
func from_nonisolated(_ x: any P) async {
3232
await x.f()
3333
x.prop += 1 // expected-error {{actor-isolated property 'prop' can not be mutated from a nonisolated context}}
34+
// expected-note@-1 {{consider declaring an 'P' actor-isolated method to perform the mutation}}
3435
x.prop = 100 // expected-error {{actor-isolated property 'prop' can not be mutated from a nonisolated context}}
36+
// expected-note@-1 {{consider declaring an 'P' actor-isolated method to perform the mutation}}
3537
}
3638

3739
func from_concrete(_ x: A) async {
3840
x.prop += 1 // expected-error {{actor-isolated property 'prop' can not be mutated from a nonisolated context}}
41+
// expected-note@-1 {{consider declaring an 'A' actor-isolated method to perform the mutation}}
3942
x.prop = 100 // expected-error {{actor-isolated property 'prop' can not be mutated from a nonisolated context}}
43+
// expected-note@-1 {{consider declaring an 'A' actor-isolated method to perform the mutation}}
4044
}
4145

4246
func from_isolated_concrete(_ x: isolated A) async {
@@ -52,8 +56,9 @@ actor Act {
5256
nonisolated let act = Act()
5357

5458
func bad() async {
55-
// expected-warning@+2 {{no 'async' operations occur within 'await' expression}}
56-
// expected-error@+1 {{actor-isolated property 'i' can not be mutated from a nonisolated context}}
59+
// expected-warning@+3 {{no 'async' operations occur within 'await' expression}}
60+
// expected-error@+2 {{actor-isolated property 'i' can not be mutated from a nonisolated context}}
61+
// expected-note@+1 {{consider declaring an 'Act' actor-isolated method to perform the mutation}}
5762
await act.i = 666
5863
}
5964

@@ -63,12 +68,14 @@ protocol Proto: Actor {
6368
extension Act: Proto {}
6469

6570
func good() async {
66-
// expected-warning@+2 {{no 'async' operations occur within 'await' expression}}
67-
// expected-error@+1 {{actor-isolated property 'i' can not be mutated from a nonisolated context}}
71+
// expected-warning@+3 {{no 'async' operations occur within 'await' expression}}
72+
// expected-error@+2 {{actor-isolated property 'i' can not be mutated from a nonisolated context}}
73+
// expected-note@+1 {{consider declaring an 'Proto' actor-isolated method to perform the mutation}}
6874
await (act as any Proto).i = 42
6975
let aIndirect: any Proto = act
7076

71-
// expected-warning@+2 {{no 'async' operations occur within 'await' expression}}
72-
// expected-error@+1 {{actor-isolated property 'i' can not be mutated from a nonisolated context}}
77+
// expected-warning@+3 {{no 'async' operations occur within 'await' expression}}
78+
// expected-error@+2 {{actor-isolated property 'i' can not be mutated from a nonisolated context}}
79+
// expected-note@+1 {{consider declaring an 'Proto' actor-isolated method to perform the mutation}}
7380
await aIndirect.i = 777
7481
}

test/Concurrency/actor_isolation.swift

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,18 +98,23 @@ func checkAsyncPropertyAccess() async {
9898
let act = MyActor()
9999
let _ : Int = await act.mutable + act.mutable
100100
act.mutable += 1 // expected-error {{actor-isolated property 'mutable' can not be mutated from a nonisolated context}}
101+
// expected-note@-1{{consider declaring an 'MyActor' actor-isolated method to perform the mutation}}
101102

102103
act.superState += 1 // expected-error {{actor-isolated property 'superState' can not be mutated from a nonisolated context}}
104+
// expected-note@-1{{consider declaring an 'MySuperActor' actor-isolated method to perform the mutation}}
103105

104106
act.text[0].append("hello") // expected-error{{actor-isolated property 'text' can not be mutated from a nonisolated context}}
107+
// expected-note@-1{{consider declaring an 'MyActor' actor-isolated method to perform the mutation}}
105108

106109
// this is not the same as the above, because Array is a value type
107110
var arr = await act.text
108111
arr[0].append("hello")
109112

110113
act.text.append("no") // expected-error{{actor-isolated property 'text' can not be mutated from a nonisolated context}}
114+
// expected-note@-1{{consider declaring an 'MyActor' actor-isolated method to perform the mutation}}
111115

112116
act.text[0] += "hello" // expected-error{{actor-isolated property 'text' can not be mutated from a nonisolated context}}
117+
// expected-note@-1{{consider declaring an 'MyActor' actor-isolated method to perform the mutation}}
113118

114119
_ = act.point // expected-warning{{non-sendable type 'Point' of property 'point' cannot exit actor-isolated context}}
115120
// expected-warning@-1 {{expression is 'async' but is not marked with 'await'}}
@@ -269,9 +274,11 @@ extension MyActor {
269274
// expected-note@-1{{property access is 'async'}}
270275
_ = await otherActor.mutable
271276
otherActor.mutable = 0 // expected-error{{actor-isolated property 'mutable' can not be mutated on a nonisolated actor instance}}
277+
// expected-note@-1{{consider declaring an 'MyActor' actor-isolated method to perform the mutation}}
272278
acceptInout(&otherActor.mutable) // expected-error{{actor-isolated property 'mutable' can not be used 'inout' on a nonisolated actor instance}}
273-
// expected-error@+2{{actor-isolated property 'mutable' can not be mutated on a nonisolated actor instance}}
274-
// expected-warning@+1{{no 'async' operations occur within 'await' expression}}
279+
// expected-error@+3{{actor-isolated property 'mutable' can not be mutated on a nonisolated actor instance}}
280+
// expected-warning@+2{{no 'async' operations occur within 'await' expression}}
281+
// expected-note@+1{{consider declaring an 'MyActor' actor-isolated method to perform the mutation}}
275282
await otherActor.mutable = 0
276283

277284
_ = otherActor.synchronous()
@@ -443,8 +450,10 @@ actor Crystal {
443450
_ = await globActorVar + globActorProp
444451

445452
globActorProp = 20 // expected-error {{global actor 'SomeGlobalActor'-isolated property 'globActorProp' can not be mutated on a different actor instance}}
453+
// expected-note@-1{{consider declaring an global actor 'SomeGlobalActor'-isolated method to perform the mutation}}
446454

447455
globActorVar = 30 // expected-error {{global actor 'SomeGlobalActor'-isolated property 'globActorVar' can not be mutated on a different actor instance}}
456+
// expected-note@-1{{consider declaring an global actor 'SomeGlobalActor'-isolated method to perform the mutation}}
448457

449458
// expected-error@+2 {{global actor 'SomeGlobalActor'-isolated property 'globActorVar' can not be used 'inout' on a different actor instance}}
450459
// expected-error@+1 {{actor-isolated property 'globActorVar' cannot be passed 'inout' to implicitly 'async' function call}}
@@ -864,7 +873,7 @@ extension SomeClassInActor.ID {
864873
@available(SwiftStdlib 5.1, *)
865874
actor SomeActorWithInits {
866875
// expected-note@+2 2 {{property declared here}}
867-
// expected-note@+1 3 {{mutation of this property is only permitted within the actor}}
876+
// expected-note@+1 4 {{mutation of this property is only permitted within the actor}}
868877
var mutableState: Int = 17
869878
var otherMutableState: Int
870879
// expected-note@+1 {{mutation of this property is only permitted within the actor}}
@@ -896,6 +905,14 @@ actor SomeActorWithInits {
896905
}
897906
local()
898907

908+
func localAsync() async {
909+
isolated() // expected-warning{{actor-isolated instance method 'isolated()' can not be referenced from a nonisolated context; this is an error in the Swift 6 language mode}}
910+
mutableState += 1 // expected-warning{{actor-isolated property 'mutableState' can not be mutated from a nonisolated context; this is an error in the Swift 6 language mode}}
911+
// expected-note@-1{{consider declaring an 'SomeActorWithInits' actor-isolated method to perform the mutation}}
912+
nonisolated()
913+
}
914+
Task { await localAsync() }
915+
899916
let _ = {
900917
defer {
901918
isolated() // expected-warning{{actor-isolated instance method 'isolated()' can not be referenced from a nonisolated context; this is an error in the Swift 6 language mode}}
@@ -990,8 +1007,7 @@ actor SomeActorWithInits {
9901007
}()
9911008
}
9921009

993-
994-
func isolated() { } // expected-note 9 {{calls to instance method 'isolated()' from outside of its actor context are implicitly asynchronous}}
1010+
func isolated() { } // expected-note 10 {{calls to instance method 'isolated()' from outside of its actor context are implicitly asynchronous}}
9951011
nonisolated func nonisolated() {}
9961012
}
9971013

@@ -1346,6 +1362,7 @@ actor Butterfly {
13461362
nonisolated init(cookies: Void) async {
13471363
self.init()
13481364
self.flapsPerSec += 1 // expected-error {{actor-isolated property 'flapsPerSec' can not be mutated from a nonisolated context}}
1365+
// expected-note@-1 {{consider declaring an 'Butterfly' actor-isolated method to perform the mutation}}
13491366
}
13501367

13511368
init(brownies: Void) {

test/Concurrency/toplevel/async-5-top-level.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func nonIsolatedAsync() async {
2525
// expected-warning@-1:5 {{main actor-isolated var 'a' can not be mutated from a nonisolated context}}
2626
// expected-warning@-2:9 {{expression is 'async' but is not marked with 'await'}}{{9-9=await }}
2727
// expected-note@-3:9 {{property access is 'async'}}
28+
// expected-note@-4 {{consider declaring an main actor-isolated method to perform the mutation}}
2829
}
2930

3031
@MainActor

test/Concurrency/toplevel/async-6-top-level.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func nonIsolatedAsync() async {
2424
// expected-error@-1:5 {{main actor-isolated var 'a' can not be mutated from a nonisolated context}}
2525
// expected-error@-2:9 {{expression is 'async' but is not marked with 'await'}}
2626
// expected-note@-3:9 {{property access is 'async'}}
27+
// expected-note@-4 {{consider declaring an main actor-isolated method to perform the mutation}}
2728
}
2829

2930
@MainActor

test/Concurrency/toplevel/no-async-6-top-level.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func nonIsolatedAsync() async {
2626
a = a + 10 // expected-error{{main actor-isolated var 'a' can not be mutated from a nonisolated context}}
2727
// expected-note@-1{{property access is 'async'}}
2828
// expected-error@-2{{expression is 'async' but is not marked with 'await'}}
29+
// expected-note@-3{{consider declaring an main actor-isolated method to perform the mutation}}
2930
}
3031

3132
@MainActor

0 commit comments

Comments
 (0)