Skip to content

Commit 5a2a27f

Browse files
committed
Provide more Fix-It guidance for concurrency-unsafe global variables (SE-0412)
When diagnosing a concurrency-unsafe global or static variable, provide Fix-Its with specific guidance and advice. This is intended to aid the workflow for folks enabling strict concurrency checking or Swift 6. There are up to three Fix-Its attached to a diagnostic about concurrency-unsafe global/static variables: * convert 'global' to a 'let' constant to make the shared state immutable, which replaces `var` with `let` * restrict 'global' to the main actor if it will only be accessed from the main thread, which adds `@MainActor` * unsafely mark %0 as concurrency-safe if all accesses are protected by an external synchronization mechanism, which adds `nonisolated(unsafe)` I fretted over two things before deciding on this path: 1. For the second note, the reality is that any global actor will suffice, but `@MainActor` is orders of magnitude more common than any other global actor, so "common case convenience" wins over "precise but less useful. 2. For the third note, `nonisolated(unsafe)` should only be used sparingly, and surfacing it via Fix-It could cause overuse. However, developers need to know about it, and this is how we do that. It comes last in the list of notes (after the better options) and says "unsafe" in not one but two places.
1 parent 0f8c478 commit 5a2a27f

13 files changed

+102
-52
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5610,16 +5610,17 @@ ERROR(shared_mutable_state_access,none,
56105610
ERROR(shared_mutable_state_decl,none,
56115611
"%kind0 is not concurrency-safe because it is non-isolated global shared "
56125612
"mutable state", (const ValueDecl *))
5613-
NOTE(shared_mutable_state_decl_note,none,
5614-
"isolate %0 to a global actor, or convert it to a 'let' constant and "
5615-
"conform it to 'Sendable'", (const ValueDecl *))
56165613
ERROR(shared_immutable_state_decl,none,
56175614
"%kind1 is not concurrency-safe because non-'Sendable' type %0 may have "
56185615
"shared mutable state",
56195616
(Type, const ValueDecl *))
5620-
NOTE(shared_immutable_state_decl_note,none,
5621-
"isolate %0 to a global actor, or conform %1 to 'Sendable'",
5622-
(const ValueDecl *, Type))
5617+
NOTE(shared_state_make_immutable,none,
5618+
"convert %0 to a 'let' constant to make the shared state immutable",
5619+
(const ValueDecl *))
5620+
NOTE(shared_state_main_actor_node,none,
5621+
"restrict %0 to the main actor if it will only be accessed from the main thread", (const ValueDecl *))
5622+
NOTE(shared_state_nonisolated_unsafe,none,
5623+
"unsafely mark %0 as concurrency-safe if all accesses are protected by an external synchronization mechanism", (const ValueDecl *))
56235624
ERROR(actor_isolated_witness,none,
56245625
"%select{|distributed }0%1 %kind2 cannot be used to satisfy %3 protocol "
56255626
"requirement",

lib/Sema/MiscDiagnostics.cpp

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3779,6 +3779,28 @@ class ReturnTypePlaceholderReplacer : public ASTWalker {
37793779

37803780
} // end anonymous namespace
37813781

3782+
SourceLoc swift::getFixItLocForVarToLet(VarDecl *var) {
3783+
// Try to find the location of the 'var' so we can produce a fixit. If
3784+
// this is a simple PatternBinding, use its location.
3785+
if (auto *PBD = var->getParentPatternBinding()) {
3786+
if (PBD->getSingleVar() == var)
3787+
return PBD->getLoc();
3788+
} else if (auto *pattern = var->getParentPattern()) {
3789+
BindingPattern *foundVP = nullptr;
3790+
pattern->forEachNode([&](Pattern *P) {
3791+
if (auto *VP = dyn_cast<BindingPattern>(P))
3792+
if (VP->getSingleVar() == var)
3793+
foundVP = VP;
3794+
});
3795+
3796+
if (foundVP && foundVP->getIntroducer() != VarDecl::Introducer::Let) {
3797+
return foundVP->getLoc();
3798+
}
3799+
}
3800+
3801+
return SourceLoc();
3802+
}
3803+
37823804
// After we have scanned the entire region, diagnose variables that could be
37833805
// declared with a narrower usage kind.
37843806
VarDeclUsageChecker::~VarDeclUsageChecker() {
@@ -3998,25 +4020,7 @@ VarDeclUsageChecker::~VarDeclUsageChecker() {
39984020
// Don't warn if we have something like "let (x,y) = ..." and 'y' was
39994021
// never mutated, but 'x' was.
40004022
!isVarDeclPartOfPBDThatHadSomeMutation(var)) {
4001-
SourceLoc FixItLoc;
4002-
4003-
// Try to find the location of the 'var' so we can produce a fixit. If
4004-
// this is a simple PatternBinding, use its location.
4005-
if (auto *PBD = var->getParentPatternBinding()) {
4006-
if (PBD->getSingleVar() == var)
4007-
FixItLoc = PBD->getLoc();
4008-
} else if (auto *pattern = var->getParentPattern()) {
4009-
BindingPattern *foundVP = nullptr;
4010-
pattern->forEachNode([&](Pattern *P) {
4011-
if (auto *VP = dyn_cast<BindingPattern>(P))
4012-
if (VP->getSingleVar() == var)
4013-
foundVP = VP;
4014-
});
4015-
4016-
if (foundVP && foundVP->getIntroducer() != VarDecl::Introducer::Let) {
4017-
FixItLoc = foundVP->getLoc();
4018-
}
4019-
}
4023+
SourceLoc FixItLoc = getFixItLocForVarToLet(var);
40204024

40214025
// If this is a parameter explicitly marked 'var', remove it.
40224026
if (FixItLoc.isInvalid()) {

lib/Sema/MiscDiagnostics.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ namespace swift {
7070
AccessLevel desiredAccess, bool isForSetter = false,
7171
bool shouldUseDefaultAccess = false);
7272

73+
/// Compute the location of the 'var' keyword for a 'var'-to-'let' Fix-It.
74+
SourceLoc getFixItLocForVarToLet(VarDecl *var);
75+
7376
/// Describes the context of a parameter, for use in diagnosing argument
7477
/// label problems.
7578
enum class ParameterContext : unsigned {

lib/Sema/TypeCheckConcurrency.cpp

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// This file implements type checking support for Swift's concurrency model.
1414
//
1515
//===----------------------------------------------------------------------===//
16+
#include "MiscDiagnostics.h"
1617
#include "TypeCheckConcurrency.h"
1718
#include "TypeCheckDistributed.h"
1819
#include "TypeCheckInvertible.h"
@@ -5019,23 +5020,41 @@ ActorIsolation ActorIsolationRequest::evaluate(
50195020
if (auto *originalVar = var->getOriginalWrappedProperty()) {
50205021
diagVar = originalVar;
50215022
}
5023+
5024+
bool diagnosed = false;
50225025
if (var->isLet()) {
50235026
auto type = var->getInterfaceType();
5024-
bool diagnosed = diagnoseIfAnyNonSendableTypes(
5027+
diagnosed = diagnoseIfAnyNonSendableTypes(
50255028
type, SendableCheckContext(var->getDeclContext()),
50265029
/*inDerivedConformance=*/Type(), /*typeLoc=*/SourceLoc(),
50275030
/*diagnoseLoc=*/var->getLoc(),
50285031
diag::shared_immutable_state_decl, diagVar);
5029-
5030-
// If we diagnosed this 'let' as non-Sendable, tack on a note
5031-
// to suggest a course of action.
5032-
if (diagnosed)
5033-
diagVar->diagnose(diag::shared_immutable_state_decl_note,
5034-
diagVar, type);
50355032
} else {
50365033
diagVar->diagnose(diag::shared_mutable_state_decl, diagVar)
50375034
.warnUntilSwiftVersion(6);
5038-
diagVar->diagnose(diag::shared_mutable_state_decl_note, diagVar);
5035+
diagnosed = true;
5036+
}
5037+
5038+
// If we diagnosed this global, tack on notes to suggest potential
5039+
// courses of action.
5040+
if (diagnosed) {
5041+
if (!var->isLet()) {
5042+
auto diag = diagVar->diagnose(diag::shared_state_make_immutable,
5043+
diagVar);
5044+
SourceLoc fixItLoc = getFixItLocForVarToLet(diagVar);
5045+
if (fixItLoc.isValid()) {
5046+
diag.fixItReplace(fixItLoc, "let");
5047+
}
5048+
}
5049+
5050+
diagVar->diagnose(diag::shared_state_main_actor_node,
5051+
diagVar)
5052+
.fixItInsert(diagVar->getAttributeInsertionLoc(false),
5053+
"@MainActor ");
5054+
diagVar->diagnose(diag::shared_state_nonisolated_unsafe,
5055+
diagVar)
5056+
.fixItInsert(diagVar->getAttributeInsertionLoc(true),
5057+
"nonisolated(unsafe) ");
50395058
}
50405059
}
50415060
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
struct Foo {
22
static let member = Bar() // expected-complete-warning {{static property 'member' is not concurrency-safe because non-'Sendable' type 'Bar' may have shared mutable state; this is an error in the Swift 6 language mode}}
3-
// expected-complete-note@-1 {{isolate 'member' to a global actor, or conform 'Bar' to 'Sendable'}}
3+
// expected-complete-note@-1 {{restrict 'member' to the main actor if it will only be accessed from the main thread}}
4+
// expected-complete-note@-2{{unsafely mark 'member' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}
45
}

test/Concurrency/actor_isolation.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import OtherActors // expected-warning{{add '@preconcurrency' to suppress 'Senda
1212

1313
let immutableGlobal: String = "hello"
1414

15-
// expected-warning@+2 {{var 'mutableGlobal' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode}}
16-
// expected-note@+1 {{isolate 'mutableGlobal' to a global actor, or convert it to a 'let' constant and conform it to 'Sendable'}}
15+
// expected-warning@+4 {{var 'mutableGlobal' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode}}
16+
// expected-note@+3 {{convert 'mutableGlobal' to a 'let' constant to make the shared state immutable}}
17+
// expected-note@+2 {{restrict 'mutableGlobal' to the main actor if it will only be accessed from the main thread}}
18+
// expected-note@+1 {{unsafely mark 'mutableGlobal' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}
1719
var mutableGlobal: String = "can't touch this" // expected-note 5{{var declared here}}
1820

1921
@available(SwiftStdlib 5.1, *)

test/Concurrency/concurrency_warnings.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ class GlobalCounter { // expected-note{{class 'GlobalCounter' does not conform t
99
}
1010

1111
let rs = GlobalCounter() // expected-warning {{let 'rs' is not concurrency-safe because non-'Sendable' type 'GlobalCounter' may have shared mutable state; this is an error in the Swift 6 language mode}}
12-
// expected-note@-1 {{isolate 'rs' to a global actor, or conform 'GlobalCounter' to 'Sendable'}}
12+
// expected-note@-1 {{restrict 'rs' to the main actor if it will only be accessed from the main thread}}
13+
// expected-note@-2 {{unsafely mark 'rs' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}
14+
1315

1416
var globalInt = 17 // expected-warning {{var 'globalInt' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode}}
15-
// expected-note@-1 {{isolate 'globalInt' to a global actor, or convert it to a 'let' constant and conform it to 'Sendable'}}
17+
// expected-note@-1 {{restrict 'globalInt' to the main actor if it will only be accessed from the main thread}}
1618
// expected-note@-2 2{{var declared here}}
17-
19+
// expected-note@-3 {{unsafely mark 'globalInt' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}
20+
// expected-note@-4 {{convert 'globalInt' to a 'let' constant to make the shared state immutable}}
1821

1922
class MyError: Error { // expected-warning{{non-final class 'MyError' cannot conform to 'Sendable'; use '@unchecked Sendable'}}
2023
var storage = 0 // expected-warning{{stored property 'storage' of 'Sendable'-conforming class 'MyError' is mutable}}

test/Concurrency/concurrent_value_checking.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,9 @@ typealias BadGenericCF<T> = @Sendable () -> T?
274274
typealias GoodGenericCF<T: Sendable> = @Sendable () -> T? // okay
275275

276276
var concurrentFuncVar: (@Sendable (NotConcurrent) -> Void)? = nil // expected-warning{{var 'concurrentFuncVar' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode}}
277-
// expected-note@-1 {{isolate 'concurrentFuncVar' to a global actor, or convert it to a 'let' constant and conform it to 'Sendable'}}
277+
// expected-note@-1 {{restrict 'concurrentFuncVar' to the main actor if it will only be accessed from the main thread}}
278+
// expected-note@-2 {{unsafely mark 'concurrentFuncVar' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}
279+
// expected-note@-3 {{convert 'concurrentFuncVar' to a 'let' constant to make the shared state immutable}}
278280

279281
// ----------------------------------------------------------------------
280282
// Sendable restriction on @Sendable closures.

test/Concurrency/flow_isolation.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,8 +520,10 @@ struct CardboardBox<T> {
520520

521521
@available(SwiftStdlib 5.1, *)
522522
var globalVar: EscapeArtist? // expected-warning {{var 'globalVar' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode}}
523-
// expected-note@-1 {{isolate 'globalVar' to a global actor, or convert it to a 'let' constant and conform it to 'Sendable'}}
523+
// expected-note@-1 {{restrict 'globalVar' to the main actor if it will only be accessed from the main thread}}
524524
// expected-note@-2 2 {{var declared here}}
525+
// expected-note@-3 {{unsafely mark 'globalVar' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}
526+
// expected-note@-4 {{convert 'globalVar' to a 'let' constant to make the shared state immutable}}
525527

526528
@available(SwiftStdlib 5.1, *)
527529
actor EscapeArtist {

test/Concurrency/freestanding_top_level.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
// RUN: %target-swift-frontend -concurrency-model=task-to-thread -typecheck -verify %s
22
// RUN: %target-swift-frontend -concurrency-model=task-to-thread -typecheck -verify -verify-additional-prefix complete- -strict-concurrency=complete %s
33

4-
// expected-complete-warning@+3 {{var 'global' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode}}
5-
// expected-complete-note@+2 {{isolate 'global' to a global actor, or convert it to a 'let' constant and conform it to 'Sendable'}}
6-
// expected-complete-note@+1 {{var declared here}}
4+
// expected-complete-warning@+5 {{var 'global' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode}}
5+
// expected-complete-note@+4 {{restrict 'global' to the main actor if it will only be accessed from the main thread}}{{1-1=@MainActor }}
6+
// expected-complete-note@+3 {{var declared here}}
7+
// expected-complete-note@+2 {{unsafely mark 'global' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}{{1-1=nonisolated(unsafe) }}
8+
// expected-complete-note@+1 {{convert 'global' to a 'let' constant to make the shared state immutable}}{{1-4=let}}
79
var global = 10
810

911
// expected-complete-warning@+1 {{reference to var 'global' is not concurrency-safe because it involves shared mutable state; this is an error in the Swift 6 language mode}}

test/Concurrency/global_variables.swift

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ actor TestGlobalActor {
1515
var mutableIsolatedGlobal = 1
1616

1717
var mutableNonisolatedGlobal = 1 // expected-error{{var 'mutableNonisolatedGlobal' is not concurrency-safe because it is non-isolated global shared mutable state}}
18-
// expected-note@-1{{isolate 'mutableNonisolatedGlobal' to a global actor, or convert it to a 'let' constant and conform it to 'Sendable'}}
18+
// expected-note@-1{{restrict 'mutableNonisolatedGlobal' to the main actor if it will only be accessed from the main thread}}{{1-1=@MainActor }}
19+
// expected-note@-2{{unsafely mark 'mutableNonisolatedGlobal' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}{{1-1=nonisolated(unsafe) }}
20+
// expected-note@-3{{convert 'mutableNonisolatedGlobal' to a 'let' constant to make the shared state immutable}}{{1-4=let}}
1921

2022
let immutableGlobal = 1
2123

@@ -46,20 +48,26 @@ actor TestActor {
4648
struct TestStatics {
4749
static let immutableExplicitSendable = TestSendable()
4850
static let immutableNonsendable = TestNonsendable() // expected-error{{static property 'immutableNonsendable' is not concurrency-safe because non-'Sendable' type 'TestNonsendable' may have shared mutable state}}
49-
// expected-note@-1 {{isolate 'immutableNonsendable' to a global actor, or conform 'TestNonsendable' to 'Sendable'}}
51+
// expected-note@-1 {{restrict 'immutableNonsendable' to the main actor if it will only be accessed from the main thread}}
52+
// expected-note@-2 {{unsafely mark 'immutableNonsendable' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}
5053
static nonisolated(unsafe) let immutableNonisolatedUnsafe = TestNonsendable()
5154
static nonisolated let immutableNonisolated = TestNonsendable() // expected-error{{static property 'immutableNonisolated' is not concurrency-safe because non-'Sendable' type 'TestNonsendable' may have shared mutable state}}
52-
// expected-note@-1 {{isolate 'immutableNonisolated' to a global actor, or conform 'TestNonsendable' to 'Sendable'}}
55+
// expected-note@-1 {{restrict 'immutableNonisolated' to the main actor if it will only be accessed from the main thread}}
5356
// expected-error@-2 {{'nonisolated' can not be applied to variable with non-'Sendable' type 'TestNonsendable'}}
57+
// expected-note@-3{{unsafely mark 'immutableNonisolated' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}
5458
static nonisolated(unsafe) let immutableNonisolatedUnsafeSendable = TestSendable()
5559
// expected-warning@-1 {{'nonisolated(unsafe)' is unnecessary for a constant with 'Sendable' type 'TestSendable', consider removing it}} {{10-30=}}
5660
static let immutableInferredSendable = 0
5761
static var mutable = 0 // expected-error{{static property 'mutable' is not concurrency-safe because it is non-isolated global shared mutable state}}
58-
// expected-note@-1{{isolate 'mutable' to a global actor, or convert it to a 'let' constant and conform it to 'Sendable'}}
62+
// expected-note@-1{{convert 'mutable' to a 'let' constant to make the shared state immutable}}
5963
// expected-note@-2{{static property declared here}}
64+
// expected-note@-3{{unsafely mark 'mutable' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}
65+
// expected-note@-4{{restrict 'mutable' to the main actor if it will only be accessed from the main thread}}
6066
static var computedProperty: Int { 0 } // computed property that, though static, has no storage so is not a global
6167
@TestWrapper static var wrapped: Int // expected-error{{static property 'wrapped' is not concurrency-safe because it is non-isolated global shared mutable state}}
62-
// expected-note@-1{{isolate 'wrapped' to a global actor, or convert it to a 'let' constant and conform it to 'Sendable'}}
68+
// expected-note@-1{{convert 'wrapped' to a 'let' constant to make the shared state immutable}}{{23-26=let}}
69+
// expected-note@-2{{unsafely mark 'wrapped' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}{{16-16=nonisolated(unsafe) }}
70+
// expected-note@-3{{restrict 'wrapped' to the main actor if it will only be accessed from the main thread}}{{3-3=@MainActor }}
6371
}
6472

6573
public actor TestPublicActor {

test/Concurrency/predates_concurrency_import.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ func test(
4444
let nonStrictGlobal = NonStrictClass() // no warning
4545

4646
let strictGlobal = StrictStruct() // expected-warning{{let 'strictGlobal' is not concurrency-safe because non-'Sendable' type 'StrictStruct' may have shared mutable state}}
47-
// expected-note@-1{{isolate 'strictGlobal' to a global actor, or conform 'StrictStruct' to 'Sendable'}}
47+
// expected-note@-1{{restrict 'strictGlobal' to the main actor if it will only be accessed from the main thread}}
48+
// expected-note@-2{{unsafely mark 'strictGlobal' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}
4849

4950
extension NonStrictClass {
5051
@Sendable func f() { }
@@ -61,5 +62,6 @@ struct HasStatics {
6162
nonisolated static let ss: StrictStruct = StrictStruct()
6263
// expected-warning@-1{{'nonisolated' can not be applied to variable with non-'Sendable' type 'StrictStruct'}}
6364
// expected-warning@-2{{static property 'ss' is not concurrency-safe because non-'Sendable' type 'StrictStruct' may have shared mutable state}}
64-
// expected-note@-3{{isolate 'ss' to a global actor, or conform 'StrictStruct' to 'Sendable'}}
65+
// expected-note@-3{{restrict 'ss' to the main actor if it will only be accessed from the main thread}}
66+
// expected-note@-4{{unsafely mark 'ss' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}
6567
}

test/Concurrency/predates_concurrency_import_swift6.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ func test(ss: StrictStruct, ns: NonStrictClass) {
1818

1919
let nonStrictGlobal = NonStrictClass()
2020
let strictGlobal = StrictStruct() // expected-warning{{let 'strictGlobal' is not concurrency-safe because non-'Sendable' type 'StrictStruct' may have shared mutable state}}
21-
// expected-note@-1{{isolate 'strictGlobal' to a global actor, or conform 'StrictStruct' to 'Sendable'}}
21+
// expected-note@-1{{restrict 'strictGlobal' to the main actor if it will only be accessed from the main thread}}
22+
// expected-note@-2{{unsafely mark 'strictGlobal' as concurrency-safe if all accesses are protected by an external synchronization mechanism}}
2223

2324
extension NonStrictClass {
2425
@Sendable func f() { }

0 commit comments

Comments
 (0)