Skip to content

Commit 35355af

Browse files
authored
Merge pull request swiftlang#59358 from apple/wip-missing-witness-allowance
[Distributed] Fix too restrictive distributed witness isolation checking
2 parents 3bd45ec + 8e6d721 commit 35355af

File tree

5 files changed

+200
-16
lines changed

5 files changed

+200
-16
lines changed

include/swift/AST/Decl.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3644,7 +3644,7 @@ class NominalTypeDecl : public GenericTypeDecl, public IterableDeclContext {
36443644
bool isActor() const;
36453645

36463646
/// Whether this nominal type qualifies as a distributed actor, meaning that
3647-
/// it is either a distributed actor.
3647+
/// it is either a distributed actor or a DistributedActor constrained protocol.
36483648
bool isDistributedActor() const;
36493649

36503650
/// Whether this nominal type qualifies as any actor (plain or distributed).

lib/AST/Decl.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9224,7 +9224,7 @@ ActorIsolation swift::getActorIsolationOfContext(DeclContext *dc) {
92249224
auto selfDecl = isolation.getActorInstance();
92259225
auto actorClass = selfDecl->getType()->getReferenceStorageReferent()
92269226
->getClassOrBoundGenericClass();
9227-
// FIXME: Doesn't work properly with generics
9227+
// FIXME: Doesn't work properly with generics #59356
92289228
assert(actorClass && "Bad closure actor isolation?");
92299229
return ActorIsolation::forActorInstance(actorClass)
92309230
.withPreconcurrency(isolation.preconcurrency());

lib/Sema/TypeCheckProtocol.cpp

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3029,14 +3029,46 @@ bool ConformanceChecker::checkActorIsolation(
30293029
bool isDistributed = refResult.isolation.isDistributedActor() &&
30303030
!witness->getAttrs().hasAttribute<NonisolatedAttr>();
30313031
if (isDistributed) {
3032-
// If we're coming from a non-distributed requirement, then the requirement
3033-
// must be 'throws' to accommodate failure.
3034-
if (!isDistributedDecl(requirement) && !isThrowsDecl(requirement))
3035-
missingOptions |= MissingFlags::RequirementThrows;
3036-
3037-
if (!isDistributedDecl(witness) &&
3038-
(isDistributedDecl(requirement) || !missingOptions))
3039-
missingOptions |= MissingFlags::WitnessDistributed;
3032+
// Check if the protocol where the requirement originates from
3033+
// is a distributed actor constrained one.
3034+
if (cast<ProtocolDecl>(requirement->getDeclContext())->isDistributedActor()) {
3035+
// The requirement was declared in a DistributedActor constrained proto.
3036+
//
3037+
// This means casting up to this `P` won't "strip off" the
3038+
// "distributed-ness" of the type, and all call-sites will be checking
3039+
// distributed isolation.
3040+
//
3041+
// This means that we can actually allow these specific requirements,
3042+
// to be witnessed without the distributed keyword (!), but they won't be
3043+
// possible to be called unless:
3044+
// - from inside the distributed actor (self),
3045+
// - on a known-to-be-local distributed actor reference.
3046+
//
3047+
// This allows us to implement protocols where a local distributed actor
3048+
// registers "call me when something happens", and that call can be
3049+
// expressed as non-distributed function which we are guaranteed to be
3050+
// able to call, since the whenLocal will give us access to this actor as
3051+
// known-to-be-local, so we can invoke this method.
3052+
3053+
// If the requirement is distributed, we still need to require it on the witness though.
3054+
// We DO allow a non-distributed requirement to be witnessed here though!
3055+
if (isDistributedDecl(requirement) && !isDistributedDecl(witness))
3056+
missingOptions |= MissingFlags::WitnessDistributed;
3057+
} else {
3058+
// The protocol requirement comes from a normal (non-distributed actor)
3059+
// protocol; so the only witnesses allowed are such that we can witness
3060+
// them using a distributed, or nonisolated functions.
3061+
3062+
// If we're coming from a non-distributed requirement,
3063+
// then the requirement must be 'throws' to accommodate failure.
3064+
if (!isThrowsDecl(requirement))
3065+
missingOptions |= MissingFlags::RequirementThrows;
3066+
3067+
// If the witness is distributed, it is able to witness a requirement
3068+
// only if the requirement is `async throws`.
3069+
if (!isDistributedDecl(witness) && !missingOptions)
3070+
missingOptions |= MissingFlags::WitnessDistributed;
3071+
}
30403072
}
30413073

30423074
// If we aren't missing anything, do a Sendable check and move on.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %target-swift-frontend-emit-module -emit-module-path %t/FakeDistributedActorSystems.swiftmodule -module-name FakeDistributedActorSystems -disable-availability-checking %S/../Inputs/FakeDistributedActorSystems.swift
3+
// RUN: %target-build-swift -module-name main -Xfrontend -enable-experimental-distributed -Xfrontend -disable-availability-checking -j2 -parse-as-library -I %t %s %S/../Inputs/FakeDistributedActorSystems.swift -o %t/a.out
4+
// RUN: %target-run %t/a.out | %FileCheck %s --color
5+
6+
// REQUIRES: executable_test
7+
// REQUIRES: concurrency
8+
// REQUIRES: distributed
9+
10+
// rdar://76038845
11+
// UNSUPPORTED: use_os_stdlib
12+
// UNSUPPORTED: back_deployment_runtime
13+
14+
// FIXME(distributed): Distributed actors currently have some issues on windows, isRemote always returns false. rdar://82593574
15+
// UNSUPPORTED: OS=windows-msvc
16+
17+
18+
import Distributed
19+
import FakeDistributedActorSystems
20+
21+
typealias DefaultDistributedActorSystem = FakeRoundtripActorSystem
22+
23+
protocol LifecycleWatch: DistributedActor where ActorSystem == FakeRoundtripActorSystem {
24+
func terminated(actor id: ID) async
25+
}
26+
27+
extension LifecycleWatch {
28+
func watch<T: Codable>(x: Int, _ y: T) async throws {
29+
// nothing here
30+
print("executed: \(#function) - x = \(x), y = \(y)")
31+
}
32+
33+
distributed func test<T: Codable & Sendable>(x: Int, _ y: T) async throws {
34+
print("executed: \(#function)")
35+
try await self.watch(x: x, y)
36+
print("done executed: \(#function)")
37+
}
38+
}
39+
40+
distributed actor Worker: LifecycleWatch {
41+
func terminated(actor id: ID) async {
42+
print("terminated (on \(self.id)): \(id)")
43+
}
44+
}
45+
46+
@main struct Main {
47+
static func main() async {
48+
let worker: any LifecycleWatch = Worker(actorSystem: DefaultDistributedActorSystem())
49+
try! await worker.test(x: 42, "on protocol")
50+
51+
// CHECK: executed: test(x:_:)
52+
// CHECK: executed: watch(x:_:) - x = 42, y = on protocol
53+
// CHECK: done executed: test(x:_:)
54+
55+
// FIXME: Actor isolation getting with generics is pending implementation #59356
56+
do {
57+
let terminatedID = Worker.ID(parse: "<terminated-id>")
58+
let __secretlyKnownToBeLocal = worker
59+
await __secretlyKnownToBeLocal.terminated(actor: terminatedID)
60+
61+
// FIXME: Once the above fixme is solved, use this real code instead:
62+
// _ = await worker.whenLocal { __secretlyKnownToBeLocal in
63+
// let terminatedID = Worker.ID(parse: "<terminated-id>")
64+
// return await __secretlyKnownToBeLocal.terminated(actor: terminatedID)
65+
// }
66+
}
67+
// CHECK: terminated (on ActorAddress(address: "<unique-id>")): ActorAddress(address: "<terminated-id>")
68+
69+
print("OK") // CHECK: OK
70+
}
71+
}

test/Distributed/distributed_protocol_isolation.swift

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,9 @@ protocol StrictlyLocal {
100100

101101
func localThrows() throws
102102
// expected-note@-1 2{{mark the protocol requirement 'localThrows()' 'async' to allow actor-isolated conformances}}{{22-22=async }}
103-
104-
// TODO: localAsync
103+
104+
func localAsync() async
105+
// expected-note@-1 2{{mark the protocol requirement 'localAsync()' 'throws' to allow actor-isolated conformances}}
105106
}
106107

107108
distributed actor Nope1_StrictlyLocal: StrictlyLocal {
@@ -111,24 +112,29 @@ distributed actor Nope1_StrictlyLocal: StrictlyLocal {
111112
func localThrows() throws {}
112113
// expected-error@-1{{distributed actor-isolated instance method 'localThrows()' cannot be used to satisfy nonisolated protocol requirement}}
113114
// expected-note@-2{{add 'nonisolated' to 'localThrows()' to make this instance method not isolated to the actor}}
115+
func localAsync() async {}
116+
// expected-error@-1{{distributed actor-isolated instance method 'localAsync()' cannot be used to satisfy nonisolated protocol requirement}}
117+
// expected-note@-2{{add 'nonisolated' to 'localAsync()' to make this instance method not isolated to the actor}}
114118
}
115119
distributed actor Nope2_StrictlyLocal: StrictlyLocal {
116120
distributed func local() {}
117121
// expected-error@-1{{actor-isolated distributed instance method 'local()' cannot be used to satisfy nonisolated protocol requirement}}
118122
distributed func localThrows() throws {}
119123
// expected-error@-1{{actor-isolated distributed instance method 'localThrows()' cannot be used to satisfy nonisolated protocol requirement}}
124+
distributed func localAsync() async {}
125+
// expected-error@-1{{actor-isolated distributed instance method 'localAsync()' cannot be used to satisfy nonisolated protocol requirement}}
120126
}
121127
distributed actor OK_StrictlyLocal: StrictlyLocal {
122128
nonisolated func local() {}
123129
nonisolated func localThrows() throws {}
130+
nonisolated func localAsync() async {}
124131
}
125132

126133
protocol Server {
127-
func send<Message: Codable>(message: Message) async throws -> String
134+
func send<Message: Codable & Sendable>(message: Message) async throws -> String
128135
}
129136
actor MyServer : Server {
130-
// expected-note@+1{{consider making generic parameter 'Message' conform to the 'Sendable' protocol}} {{29-29=, Sendable}}
131-
func send<Message: Codable>(message: Message) throws -> String { "" } // expected-warning{{non-sendable type 'Message' in parameter of actor-isolated instance method 'send(message:)' satisfying protocol requirement cannot cross actor boundary}}
137+
func send<Message: Codable & Sendable>(message: Message) throws -> String { "" } // OK
132138
}
133139

134140
protocol AsyncThrowsAll {
@@ -140,9 +146,15 @@ actor LocalOK_AsyncThrowsAll: AsyncThrowsAll {
140146
func maybe(param: String, int: Int) async throws -> Int { 1111 }
141147
}
142148

143-
actor LocalOK_Implicitly_AsyncThrowsAll: AsyncThrowsAll {
149+
actor LocalOK_ImplicitlyThrows_AsyncThrowsAll: AsyncThrowsAll {
150+
func maybe(param: String, int: Int) async -> Int { 1111 }
151+
}
152+
actor LocalOK_ImplicitlyAsync_AsyncThrowsAll: AsyncThrowsAll {
144153
func maybe(param: String, int: Int) throws -> Int { 1111 }
145154
}
155+
actor LocalOK_ImplicitlyThrowsAsync_AsyncThrowsAll: AsyncThrowsAll {
156+
func maybe(param: String, int: Int) -> Int { 1111 }
157+
}
146158

147159
distributed actor Nope1_AsyncThrowsAll: AsyncThrowsAll {
148160
func maybe(param: String, int: Int) async throws -> Int { 111 }
@@ -170,6 +182,75 @@ func testAsyncThrowsAll(p: AsyncThrowsAll,
170182
_ = try await pp.maybe(param: "", int: 0)
171183
}
172184

185+
// ==== -----------------------------------------------------------------------
186+
// MARK: Distributed actor protocols can have non-dist requirements
187+
188+
protocol TerminationWatchingA {
189+
func terminated(a: String) async
190+
// expected-note@-1{{mark the protocol requirement 'terminated(a:)' 'throws' to allow actor-isolated conformances}}
191+
}
192+
193+
protocol TerminationWatchingDA: DistributedActor {
194+
func terminated(da: String) async // expected-note 3 {{distributed actor-isolated instance method 'terminated(da:)' declared here}}
195+
}
196+
197+
actor A_TerminationWatchingA: TerminationWatchingA {
198+
func terminated(a: String) { } // ok, since: actor -> implicitly async
199+
}
200+
func test_watching_A(a: A_TerminationWatchingA) async throws {
201+
await a.terminated(a: "normal")
202+
}
203+
204+
distributed actor DA_TerminationWatchingA: TerminationWatchingA {
205+
func terminated(a: String) { }
206+
// expected-error@-1{{distributed actor-isolated instance method 'terminated(a:)' cannot be used to satisfy nonisolated protocol requirement}}
207+
// expected-note@-2{{add 'nonisolated' to 'terminated(a:)' to make this instance method not isolated to the actor}}
208+
}
209+
210+
distributed actor DA_TerminationWatchingDA: TerminationWatchingDA {
211+
distributed func test() {}
212+
func terminated(da: String) { }
213+
// expected-note@-1{{distributed actor-isolated instance method 'terminated(da:)' declared here}}
214+
}
215+
216+
func test_watchingDA(da: DA_TerminationWatchingDA) async throws {
217+
try await da.test() // ok
218+
da.terminated(da: "the terminated func is not distributed") // expected-error{{only 'distributed' instance methods can be called on a potentially remote distributed actor}}
219+
}
220+
221+
func test_watchingDA<WDA: TerminationWatchingDA>(da: WDA) async throws {
222+
try await da.terminated(da: "the terminated func is not distributed")
223+
// expected-error@-1{{only 'distributed' instance methods can be called on a potentially remote distributed actor}}
224+
// expected-warning@-2{{no calls to throwing functions occur within 'try' expression}}
225+
226+
let __secretlyKnownToBeLocal = da
227+
await __secretlyKnownToBeLocal.terminated(da: "local calls are okey!") // OK // FIXME(#59356): (the __secretlyKnown is a hack, but the whenLocal crashes now on pending isolation getting with generic actors for closures)
228+
// FIXME: pending fix of closure isolation checking with actors #59356
229+
// await da.whenLocal { __secretlyKnownToBeLocal in
230+
// await __secretlyKnownToBeLocal.terminated(da: "local calls are okey!") // OK
231+
// }
232+
}
233+
234+
func test_watchingDA_erased(da: DA_TerminationWatchingDA) async throws {
235+
let wda: any TerminationWatchingDA = da
236+
try await wda.terminated(da: "the terminated func is not distributed")
237+
// expected-error@-1{{only 'distributed' instance methods can be called on a potentially remote distributed actor}}
238+
// expected-warning@-2{{no calls to throwing functions occur within 'try' expression}}
239+
240+
let __secretlyKnownToBeLocal = wda
241+
await __secretlyKnownToBeLocal.terminated(da: "local calls are okey!") // OK // FIXME(#59356): (the __secretlyKnown is a hack, but the whenLocal crashes now on pending isolation getting with generic actors for closures)
242+
// FIXME: pending fix of closure isolation checking with actors #59356
243+
// await wda.whenLocal { __secretlyKnownToBeLocal in
244+
// await __secretlyKnownToBeLocal.terminated(da: "local calls are okey!") // OK
245+
// }
246+
}
247+
248+
func test_watchingDA_any(da: any TerminationWatchingDA) async throws {
249+
try await da.terminated(da: "the terminated func is not distributed")
250+
// expected-error@-1{{only 'distributed' instance methods can be called on a potentially remote distributed actor}}
251+
// expected-warning@-2{{no calls to throwing functions occur within 'try' expression}}
252+
}
253+
173254
// ==== ------------------------------------------------------------------------
174255
// MARK: Error cases
175256

0 commit comments

Comments
 (0)