Skip to content

Commit 64b8624

Browse files
authored
🍒[5.7][Distributed] Fix too restrictive distributed witness isolation checking (#59397)
* [Distributed] Implement missing case in permitting witnesses * improve FIXME to link to issue * workaround for #59356 while still implementing the witness feature * [Distributed] Further witness checking cleanup and tests
1 parent 235a571 commit 64b8624

File tree

5 files changed

+200
-15
lines changed

5 files changed

+200
-15
lines changed

‎include/swift/AST/Decl.h

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

36363636
/// Whether this nominal type qualifies as a distributed actor, meaning that
3637-
/// it is either a distributed actor.
3637+
/// it is either a distributed actor or a DistributedActor constrained protocol.
36383638
bool isDistributedActor() const;
36393639

36403640
/// 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
@@ -9154,7 +9154,7 @@ ActorIsolation swift::getActorIsolationOfContext(DeclContext *dc) {
91549154
auto selfDecl = isolation.getActorInstance();
91559155
auto actorClass = selfDecl->getType()->getReferenceStorageReferent()
91569156
->getClassOrBoundGenericClass();
9157-
// FIXME: Doesn't work properly with generics
9157+
// FIXME: Doesn't work properly with generics #59356
91589158
assert(actorClass && "Bad closure actor isolation?");
91599159
return ActorIsolation::forActorInstance(actorClass)
91609160
.withPreconcurrency(isolation.preconcurrency());

‎lib/Sema/TypeCheckProtocol.cpp

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3008,14 +3008,46 @@ bool ConformanceChecker::checkActorIsolation(
30083008
bool isDistributed = refResult.isolation.isDistributedActor() &&
30093009
!witness->getAttrs().hasAttribute<NonisolatedAttr>();
30103010
if (isDistributed) {
3011-
// If we're coming from a non-distributed requirement, then the requirement
3012-
// must be 'throws' to accommodate failure.
3013-
if (!isDistributedDecl(requirement) && !isThrowsDecl(requirement))
3014-
missingOptions |= MissingFlags::RequirementThrows;
3015-
3016-
if (!isDistributedDecl(witness) &&
3017-
(isDistributedDecl(requirement) || !missingOptions))
3018-
missingOptions |= MissingFlags::WitnessDistributed;
3011+
// Check if the protocol where the requirement originates from
3012+
// is a distributed actor constrained one.
3013+
if (cast<ProtocolDecl>(requirement->getDeclContext())->isDistributedActor()) {
3014+
// The requirement was declared in a DistributedActor constrained proto.
3015+
//
3016+
// This means casting up to this `P` won't "strip off" the
3017+
// "distributed-ness" of the type, and all call-sites will be checking
3018+
// distributed isolation.
3019+
//
3020+
// This means that we can actually allow these specific requirements,
3021+
// to be witnessed without the distributed keyword (!), but they won't be
3022+
// possible to be called unless:
3023+
// - from inside the distributed actor (self),
3024+
// - on a known-to-be-local distributed actor reference.
3025+
//
3026+
// This allows us to implement protocols where a local distributed actor
3027+
// registers "call me when something happens", and that call can be
3028+
// expressed as non-distributed function which we are guaranteed to be
3029+
// able to call, since the whenLocal will give us access to this actor as
3030+
// known-to-be-local, so we can invoke this method.
3031+
3032+
// If the requirement is distributed, we still need to require it on the witness though.
3033+
// We DO allow a non-distributed requirement to be witnessed here though!
3034+
if (isDistributedDecl(requirement) && !isDistributedDecl(witness))
3035+
missingOptions |= MissingFlags::WitnessDistributed;
3036+
} else {
3037+
// The protocol requirement comes from a normal (non-distributed actor)
3038+
// protocol; so the only witnesses allowed are such that we can witness
3039+
// them using a distributed, or nonisolated functions.
3040+
3041+
// If we're coming from a non-distributed requirement,
3042+
// then the requirement must be 'throws' to accommodate failure.
3043+
if (!isThrowsDecl(requirement))
3044+
missingOptions |= MissingFlags::RequirementThrows;
3045+
3046+
// If the witness is distributed, it is able to witness a requirement
3047+
// only if the requirement is `async throws`.
3048+
if (!isDistributedDecl(witness) && !missingOptions)
3049+
missingOptions |= MissingFlags::WitnessDistributed;
3050+
}
30193051
}
30203052

30213053
// 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 & 5 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,23 +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-
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
131138
}
132139

133140
protocol AsyncThrowsAll {
@@ -139,9 +146,15 @@ actor LocalOK_AsyncThrowsAll: AsyncThrowsAll {
139146
func maybe(param: String, int: Int) async throws -> Int { 1111 }
140147
}
141148

142-
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 {
143153
func maybe(param: String, int: Int) throws -> Int { 1111 }
144154
}
155+
actor LocalOK_ImplicitlyThrowsAsync_AsyncThrowsAll: AsyncThrowsAll {
156+
func maybe(param: String, int: Int) -> Int { 1111 }
157+
}
145158

146159
distributed actor Nope1_AsyncThrowsAll: AsyncThrowsAll {
147160
func maybe(param: String, int: Int) async throws -> Int { 111 }
@@ -169,6 +182,75 @@ func testAsyncThrowsAll(p: AsyncThrowsAll,
169182
_ = try await pp.maybe(param: "", int: 0)
170183
}
171184

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+
172254
// ==== ------------------------------------------------------------------------
173255
// MARK: Error cases
174256

0 commit comments

Comments
 (0)