Skip to content

Commit f1218fc

Browse files
authored
Merge pull request swiftlang#36391 from kavon/actor-keypaths
2 parents f240b95 + 34a0c71 commit f1218fc

File tree

6 files changed

+288
-14
lines changed

6 files changed

+288
-14
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4331,6 +4331,9 @@ ERROR(actor_isolated_from_async_let,none,
43314331
ERROR(actor_isolated_from_escaping_closure,none,
43324332
"actor-isolated %0 %1 cannot be %select{referenced|mutated|used 'inout'}2 from an '@escaping' closure",
43334333
(DescriptiveDeclKind, DeclName, unsigned))
4334+
ERROR(actor_isolated_keypath_component,none,
4335+
"cannot form key path to actor-isolated %0 %1",
4336+
(DescriptiveDeclKind, DeclName))
43344337
ERROR(local_function_executed_concurrently,none,
43354338
"concurrently-executed %0 %1 must be marked as '@concurrent'",
43364339
(DescriptiveDeclKind, DeclName))
@@ -4377,6 +4380,9 @@ WARNING(non_concurrent_property_type,none,
43774380
WARNING(non_concurrent_keypath_capture,none,
43784381
"cannot form key path that captures non-concurrent-value type %0",
43794382
(Type))
4383+
WARNING(non_concurrent_keypath_access,none,
4384+
"cannot form key path that accesses non-concurrent-value type %0",
4385+
(Type))
43804386
ERROR(non_concurrent_type_member,none,
43814387
"%select{stored property %1|associated value %1}0 of "
43824388
"'ConcurrentValue'-conforming %2 %3 has non-concurrent-value type %4",

include/swift/AST/Expr.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5654,6 +5654,26 @@ class KeyPathExpr : public Expr {
56545654
llvm_unreachable("unhandled kind");
56555655
}
56565656

5657+
bool hasDeclRef() const {
5658+
switch (getKind()) {
5659+
case Kind::Property:
5660+
case Kind::Subscript:
5661+
return true;
5662+
5663+
case Kind::Invalid:
5664+
case Kind::UnresolvedProperty:
5665+
case Kind::UnresolvedSubscript:
5666+
case Kind::OptionalChain:
5667+
case Kind::OptionalWrap:
5668+
case Kind::OptionalForce:
5669+
case Kind::Identity:
5670+
case Kind::TupleElement:
5671+
case Kind::DictionaryKey:
5672+
return false;
5673+
}
5674+
llvm_unreachable("unhandled kind");
5675+
}
5676+
56575677
ConcreteDeclRef getDeclRef() const {
56585678
switch (getKind()) {
56595679
case Kind::Property:

lib/Sema/TypeCheckConcurrency.cpp

Lines changed: 85 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,21 +1266,9 @@ namespace {
12661266
}
12671267
}
12681268

1269-
// Key paths require any captured values to be ConcurrentValue-conforming.
1270-
if (auto keyPath = dyn_cast<KeyPathExpr>(expr)) {
1271-
for (const auto &component : keyPath->getComponents()) {
1272-
auto indexExpr = component.getIndexExpr();
1273-
if (!indexExpr || !indexExpr->getType())
1274-
continue;
12751269

1276-
if (shouldDiagnoseNonConcurrentValueViolations(ctx.LangOpts) &&
1277-
!isConcurrentValueType(getDeclContext(), indexExpr->getType())) {
1278-
ctx.Diags.diagnose(
1279-
component.getLoc(), diag::non_concurrent_keypath_capture,
1280-
indexExpr->getType());
1281-
}
1282-
}
1283-
}
1270+
if (auto keyPath = dyn_cast<KeyPathExpr>(expr))
1271+
checkKeyPathExpr(keyPath);
12841272

12851273
// The children of #selector expressions are not evaluated, so we do not
12861274
// need to do isolation checking there. This is convenient because such
@@ -1868,6 +1856,89 @@ namespace {
18681856
return true;
18691857
}
18701858

1859+
///
1860+
/// \return true iff a diagnostic was emitted
1861+
bool checkKeyPathExpr(KeyPathExpr *keyPath) {
1862+
bool diagnosed = false;
1863+
1864+
// returns None if it is not a 'let'-bound var decl. Otherwise,
1865+
// the bool indicates whether a diagnostic was emitted.
1866+
auto checkLetBoundVarDecl = [&](KeyPathExpr::Component const& component)
1867+
-> Optional<bool> {
1868+
auto decl = component.getDeclRef().getDecl();
1869+
if (auto varDecl = dyn_cast<VarDecl>(decl)) {
1870+
if (varDecl->isLet()) {
1871+
auto type = component.getComponentType();
1872+
if (shouldDiagnoseNonConcurrentValueViolations(ctx.LangOpts)
1873+
&& !isConcurrentValueType(getDeclContext(), type)) {
1874+
ctx.Diags.diagnose(
1875+
component.getLoc(), diag::non_concurrent_keypath_access,
1876+
type);
1877+
return true;
1878+
}
1879+
return false;
1880+
}
1881+
}
1882+
return None;
1883+
};
1884+
1885+
// check the components of the keypath.
1886+
for (const auto &component : keyPath->getComponents()) {
1887+
// The decl referred to by the path component cannot be within an actor.
1888+
if (component.hasDeclRef()) {
1889+
auto concDecl = component.getDeclRef();
1890+
auto isolation = ActorIsolationRestriction::forDeclaration(concDecl);
1891+
1892+
switch (isolation.getKind()) {
1893+
case ActorIsolationRestriction::Unsafe:
1894+
case ActorIsolationRestriction::Unrestricted:
1895+
break; // OK. Does not refer to an actor-isolated member.
1896+
1897+
case ActorIsolationRestriction::GlobalActorUnsafe:
1898+
// Only check if we're in code that's adopted concurrency features.
1899+
if (!shouldDiagnoseExistingDataRaces(getDeclContext()))
1900+
break; // do not check
1901+
1902+
LLVM_FALLTHROUGH; // otherwise, perform checking
1903+
1904+
case ActorIsolationRestriction::GlobalActor:
1905+
case ActorIsolationRestriction::CrossActorSelf:
1906+
// 'let'-bound decls with this isolation are OK, just check them.
1907+
if (auto wasLetBound = checkLetBoundVarDecl(component)) {
1908+
diagnosed = wasLetBound.getValue();
1909+
break;
1910+
}
1911+
LLVM_FALLTHROUGH; // otherwise, it's invalid so diagnose it.
1912+
1913+
case ActorIsolationRestriction::ActorSelf: {
1914+
auto decl = concDecl.getDecl();
1915+
ctx.Diags.diagnose(component.getLoc(),
1916+
diag::actor_isolated_keypath_component,
1917+
decl->getDescriptiveKind(), decl->getName());
1918+
diagnosed = true;
1919+
break;
1920+
}
1921+
}; // end switch
1922+
}
1923+
1924+
// Captured values in a path component must conform to ConcurrentValue.
1925+
// These captured values appear in Subscript, aka "index" components,
1926+
// such as \Type.dict[k] where k is a captured dictionary key.
1927+
if (auto indexExpr = component.getIndexExpr()) {
1928+
auto type = indexExpr->getType();
1929+
if (type && shouldDiagnoseNonConcurrentValueViolations(ctx.LangOpts)
1930+
&& !isConcurrentValueType(getDeclContext(), type)) {
1931+
ctx.Diags.diagnose(
1932+
component.getLoc(), diag::non_concurrent_keypath_capture,
1933+
indexExpr->getType());
1934+
diagnosed = true;
1935+
}
1936+
}
1937+
}
1938+
1939+
return diagnosed;
1940+
}
1941+
18711942
/// Check a reference to a local or global.
18721943
bool checkNonMemberReference(
18731944
ConcreteDeclRef valueRef, SourceLoc loc, DeclRefExpr *declRefExpr) {
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// RUN: %target-run-simple-swift(-Xfrontend -enable-experimental-concurrency %import-libdispatch)
2+
3+
// REQUIRES: executable_test
4+
// REQUIRES: concurrency
5+
// REQUIRES: libdispatch
6+
7+
actor Page {
8+
let initialNumWords : Int
9+
10+
@actorIndependent(unsafe)
11+
var numWords : Int
12+
13+
init(_ words : Int) {
14+
numWords = words
15+
initialNumWords = words
16+
}
17+
}
18+
19+
actor Book {
20+
let pages : [Page]
21+
22+
init(_ numPages : Int) {
23+
var stack : [Page] = []
24+
for i in 0 ..< numPages {
25+
stack.append(Page(i))
26+
}
27+
pages = stack
28+
}
29+
30+
@actorIndependent
31+
subscript(_ page : Int) -> Page {
32+
return pages[page]
33+
}
34+
}
35+
36+
func bookHasChanged(_ b : Book,
37+
currentGetter : KeyPath<Page, Int>,
38+
initialGetter : (Page) -> Int) -> Bool {
39+
let numPages = b[keyPath: \.pages.count]
40+
41+
for i in 0 ..< numPages {
42+
let pageGetter = \Book.[i]
43+
let currentWords = pageGetter.appending(path: currentGetter)
44+
45+
if (b[keyPath: currentWords] != initialGetter(b[keyPath: pageGetter])) {
46+
return true
47+
}
48+
}
49+
50+
return false
51+
}
52+
53+
func enumeratePageKeys(from : Int, to : Int) -> [KeyPath<Book, Page>] {
54+
var keys : [KeyPath<Book, Page>] = []
55+
for i in from ..< to {
56+
keys.append(\Book.[i])
57+
}
58+
return keys
59+
}
60+
61+
func erasePages(_ book : Book, badPages: [KeyPath<Book, Page>]) {
62+
for page in badPages {
63+
book[keyPath: page].numWords = 0
64+
}
65+
}
66+
67+
let book = Book(100)
68+
69+
if bookHasChanged(book, currentGetter: \Page.numWords, initialGetter: \Page.initialNumWords) {
70+
fatalError("book should not be changed")
71+
}
72+
73+
erasePages(book, badPages: enumeratePageKeys(from: 0, to: 100))
74+
75+
guard bookHasChanged(book, currentGetter: \Page.numWords, initialGetter: \Page.initialNumWords) else {
76+
fatalError("book should be wiped!")
77+
}

test/Concurrency/actor_isolation_objc.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,22 @@ actor A {
1414
}
1515

1616
func keypaths() {
17+
_ = #keyPath(A.w) // expected-error {{'#keyPath' refers to non-'@objc' property 'w'}}
18+
19+
// expected-error@+1 {{cannot form key path to actor-isolated property 'x'}}
1720
_ = #keyPath(A.x) // expected-error{{argument of '#keyPath' refers to non-'@objc' property 'x'}}
21+
22+
// expected-error@+1 {{cannot form key path to actor-isolated property 'y'}}
1823
_ = #keyPath(A.y) // expected-error{{argument of '#keyPath' refers to non-'@objc' property 'y'}}
24+
25+
// expected-error@+1 {{cannot form key path to actor-isolated property 'computed'}}
1926
_ = #keyPath(A.computed) // expected-error{{argument of '#keyPath' refers to non-'@objc' property 'computed'}}
27+
2028
_ = #keyPath(A.z)
2129
}
2230

31+
let w: Int = 0 // expected-note{{add '@objc' to expose this property to Objective-C}}
32+
2333
var x: Int = 0 // expected-note{{add '@objc' to expose this property to Objective-C}}
2434

2535
@objc var y: Int = 0 // expected-note{{add '@objc' to expose this property to Objective-C}}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// RUN: %target-typecheck-verify-swift -enable-experimental-concurrency
2+
// REQUIRES: concurrency
3+
4+
class Box {
5+
let size : Int = 0
6+
}
7+
8+
actor Door {
9+
let immutable : Int = 0
10+
let letBox : Box? = nil
11+
let letDict : [Int : Box] = [:]
12+
let immutableNeighbor : Door? = nil
13+
14+
15+
var mutableNeighbor : Door? = nil
16+
var varDict : [Int : Box] = [:]
17+
var mutable : Int = 0
18+
var varBox : Box = Box()
19+
var getOnlyInt : Int {
20+
get { 0 }
21+
}
22+
23+
@actorIndependent(unsafe) var unsafeIndependent : Int = 0
24+
25+
@MainActor var globActor_mutable : Int = 0
26+
@MainActor let globActor_immutable : Int = 0
27+
28+
@MainActor(unsafe) var unsafeGlobActor_mutable : Int = 0
29+
@MainActor(unsafe) let unsafeGlobActor_immutable : Int = 0
30+
31+
subscript(byIndex: Int) -> Int { 0 }
32+
33+
@MainActor subscript(byName: String) -> Int { 0 }
34+
35+
@actorIndependent subscript(byIEEE754: Double) -> Int { 0 }
36+
}
37+
38+
func attemptAccess<T, V>(_ t : T, _ f : (T) -> V) -> V {
39+
return f(t)
40+
}
41+
42+
func tryKeyPathsMisc(d : Door) {
43+
// as a func
44+
_ = attemptAccess(d, \Door.mutable) // expected-error {{cannot form key path to actor-isolated property 'mutable'}}
45+
_ = attemptAccess(d, \Door.immutable)
46+
_ = attemptAccess(d, \Door.immutableNeighbor?.immutableNeighbor)
47+
48+
// in combination with other key paths
49+
50+
_ = (\Door.letBox).appending(path: // expected-warning {{cannot form key path that accesses non-concurrent-value type 'Box?'}}
51+
\Box?.?.size)
52+
53+
_ = (\Door.varBox).appending(path: // expected-error {{cannot form key path to actor-isolated property 'varBox'}}
54+
\Box.size)
55+
56+
}
57+
58+
func tryKeyPathsFromAsync() async {
59+
_ = \Door.unsafeGlobActor_immutable
60+
_ = \Door.unsafeGlobActor_mutable // expected-error{{cannot form key path to actor-isolated property 'unsafeGlobActor_mutable'}}
61+
}
62+
63+
func tryNonConcurrentValue() {
64+
_ = \Door.letDict[0] // expected-warning {{cannot form key path that accesses non-concurrent-value type '[Int : Box]'}}
65+
_ = \Door.varDict[0] // expected-error {{cannot form key path to actor-isolated property 'varDict'}}
66+
_ = \Door.letBox!.size // expected-warning {{cannot form key path that accesses non-concurrent-value type 'Box?'}}
67+
}
68+
69+
func tryKeypaths() {
70+
_ = \Door.unsafeGlobActor_immutable
71+
_ = \Door.unsafeGlobActor_mutable
72+
73+
_ = \Door.immutable
74+
_ = \Door.unsafeIndependent
75+
_ = \Door.globActor_immutable
76+
_ = \Door.[4.2]
77+
_ = \Door.immutableNeighbor?.immutableNeighbor?.immutableNeighbor
78+
79+
_ = \Door.varBox // expected-error{{cannot form key path to actor-isolated property 'varBox'}}
80+
_ = \Door.mutable // expected-error{{cannot form key path to actor-isolated property 'mutable'}}
81+
_ = \Door.getOnlyInt // expected-error{{cannot form key path to actor-isolated property 'getOnlyInt'}}
82+
_ = \Door.mutableNeighbor?.mutableNeighbor?.mutableNeighbor // expected-error 3 {{cannot form key path to actor-isolated property 'mutableNeighbor'}}
83+
84+
let _ : PartialKeyPath<Door> = \.mutable // expected-error{{cannot form key path to actor-isolated property 'mutable'}}
85+
let _ : AnyKeyPath = \Door.mutable // expected-error{{cannot form key path to actor-isolated property 'mutable'}}
86+
87+
_ = \Door.globActor_mutable // expected-error{{cannot form key path to actor-isolated property 'globActor_mutable'}}
88+
_ = \Door.[0] // expected-error{{cannot form key path to actor-isolated subscript 'subscript(_:)'}}
89+
_ = \Door.["hello"] // expected-error{{cannot form key path to actor-isolated subscript 'subscript(_:)'}}
90+
}

0 commit comments

Comments
 (0)