Skip to content

Commit 339e8c6

Browse files
authored
[6.0][Distributed] Avoid infinite recursion in distributed thunk on protocol extension (#73033)
* [Distributed] Avoid infinite recursion in distributed thunk on protocol extensions Because of the new ways we handle distributed requirements to support distributed resolvable protocols, we now have protocol requirements for both the thunk and the real method. A distributed thunk inside a protocol extension now ended up calling "itself" through protocol witness, but instead should always call the "real" method. Without this patch such situation would end up in an infinite recurstion executing such method from executeDistributedTarget when the method was a default impl provided by extension on a protocol. This patch resolves this by becoming contextually aware of the distributed thunk, and allowing it to always call the "real" method. resolves rdar://125445347 * move should... method to Callee
1 parent cc09ca3 commit 339e8c6

File tree

3 files changed

+137
-26
lines changed

3 files changed

+137
-26
lines changed

lib/SILGen/SILGenApply.cpp

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,69 @@ class Callee {
616616
return result;
617617
}
618618

619+
620+
/// Determine if the target `func` should be replaced with a
621+
/// 'distributed thunk'.
622+
///
623+
/// This only applies to distributed functions when calls are made cross-actor
624+
/// isolation. One notable exception is a distributed thunk calling the "real
625+
/// underlying method", in which case (to avoid the thunk calling into itself,
626+
/// the real method must be called).
627+
///
628+
/// Witness calls which may need to be replaced with a distributed thunk call
629+
/// happen either when the target type is generic, or if we are inside an
630+
/// extension on a protocol. This method checks if we are in a context
631+
/// where we should be calling the distributed thunk of the `func` or not.
632+
/// Notably, if we are inside a distributed thunk already and are trying to
633+
/// apply distributed method calls, all those must be to the "real" method,
634+
/// because the thunks' responsibility is to call the real method, so this
635+
/// replacement cannot be applied (or we'd recursively keep calling the same
636+
/// thunk via witness).
637+
///
638+
/// In situations which do not use a witness call, distributed methods are always
639+
/// invoked Direct, and never ClassMethod, because distributed are effectively
640+
/// final.
641+
///
642+
/// \param constant the target that we want to dispatch to
643+
/// \return true when the function should be considered for replacement
644+
/// with distributed thunk when applying it
645+
bool shouldDispatchWitnessViaDistributedThunk(
646+
SILGenFunction &SGF,
647+
std::optional<SILDeclRef> constant
648+
) const {
649+
if (!constant.has_value())
650+
return false;
651+
652+
auto func = dyn_cast<FuncDecl>(constant->getDecl());
653+
if (!func)
654+
return false;
655+
656+
auto isDistributedFuncOrAccessor =
657+
func->isDistributed();
658+
if (auto acc = dyn_cast<AccessorDecl>(func)) {
659+
isDistributedFuncOrAccessor =
660+
acc->getStorage()->isDistributed();
661+
}
662+
663+
if (!isDistributedFuncOrAccessor)
664+
return false;
665+
666+
// If we are inside a distributed thunk, we want to call the "real" method,
667+
// in order to avoid infinitely recursively calling the thunk from itself.
668+
if (SGF.F.isDistributed() && SGF.F.isThunk())
669+
return false;
670+
671+
// If caller and called func are isolated to the same (distributed) actor,
672+
// (i.e. we are "inside the distributed actor"), there is no need to call
673+
// the thunk.
674+
if (isSameActorIsolated(func, SGF.FunctionDC))
675+
return false;
676+
677+
// In all other situations, we may have to replace the called function,
678+
// depending on isolation (to be checked in SILGenApply).
679+
return true;
680+
}
681+
619682
ManagedValue getFnValue(SILGenFunction &SGF,
620683
std::optional<ManagedValue> borrowedSelf) const & {
621684
std::optional<SILDeclRef> constant = std::nullopt;
@@ -681,22 +744,12 @@ class Callee {
681744
return fn;
682745
}
683746
case Kind::WitnessMethod: {
684-
if (auto func = constant->getFuncDecl()) {
685-
auto isDistributedFuncOrAccessor =
686-
func->isDistributed();
687-
if (auto acc = dyn_cast<AccessorDecl>(func)) {
688-
isDistributedFuncOrAccessor =
689-
acc->getStorage()->isDistributed();
690-
}
691-
if (isa<ProtocolDecl>(func->getDeclContext()) && isDistributedFuncOrAccessor) {
692-
// If we're calling cross-actor, we must always use a distributed thunk
693-
if (!isSameActorIsolated(func, SGF.FunctionDC)) {
694-
// the protocol witness must always be a distributed thunk, as we
695-
// may be crossing a remote boundary here.
696-
auto thunk = func->getDistributedThunk();
697-
constant = SILDeclRef(thunk).asDistributed();
698-
}
699-
}
747+
if (shouldDispatchWitnessViaDistributedThunk(SGF, constant)) {
748+
auto func = dyn_cast<FuncDecl>(constant->getDecl());
749+
assert(func); // guaranteed be non-null if shouldDispatch returned true
750+
751+
auto thunk = func->getDistributedThunk();
752+
constant = SILDeclRef(thunk).asDistributed();
700753
}
701754

702755
auto constantInfo =
@@ -783,12 +836,8 @@ class Callee {
783836
}
784837
case Kind::WitnessMethod: {
785838
if (auto func = constant->getFuncDecl()) {
786-
if (func->isDistributed() && isa<ProtocolDecl>(func->getDeclContext())) {
787-
// If we're calling cross-actor, we must always use a distributed thunk
788-
if (!isSameActorIsolated(func, SGF.FunctionDC)) {
789-
/// We must adjust the constant to use a distributed thunk.
790-
constant = constant->asDistributed();
791-
}
839+
if (shouldDispatchWitnessViaDistributedThunk(SGF, constant)) {
840+
constant = constant->asDistributed();
792841
}
793842
}
794843

test/Distributed/Runtime/distributed_actor_func_calls_remoteCall_extension.swift

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,28 @@ import FakeDistributedActorSystems
1919

2020
typealias DefaultDistributedActorSystem = FakeRoundtripActorSystem
2121

22-
extension FakeRoundtripActorSystemDistributedActor {
22+
protocol KappaProtocol : DistributedActor where ActorSystem == FakeRoundtripActorSystem {
23+
distributed func echo(name: String) -> String
24+
}
25+
26+
extension KappaProtocol {
2327
distributed func echo(name: String) -> String {
2428
return "Echo: \(name)"
2529
}
2630
}
2731

32+
distributed actor KappaProtocolImpl: KappaProtocol {
33+
// empty, gets default impl from extension on protocol
34+
}
35+
2836
func test() async throws {
2937
let system = DefaultDistributedActorSystem()
3038

31-
let local = FakeRoundtripActorSystemDistributedActor(actorSystem: system)
32-
let ref = try FakeRoundtripActorSystemDistributedActor.resolve(id: local.id, using: system)
39+
let local = KappaProtocolImpl(actorSystem: system)
40+
let ref = try KappaProtocolImpl.resolve(id: local.id, using: system)
3341

3442
let reply = try await ref.echo(name: "Caplin")
35-
// CHECK: >> remoteCall: on:main.FakeRoundtripActorSystemDistributedActor, target:main.FakeRoundtripActorSystemDistributedActor.echo(name:), invocation:FakeInvocationEncoder(genericSubs: [], arguments: ["Caplin"], returnType: Optional(Swift.String), errorType: nil), throwing:Swift.Never, returning:Swift.String
43+
// CHECK: >> remoteCall: on:main.KappaProtocolImpl, target:main.$KappaProtocol.echo(name:), invocation:FakeInvocationEncoder(genericSubs: [main.KappaProtocolImpl], arguments: ["Caplin"], returnType: Optional(Swift.String), errorType: nil), throwing:Swift.Never, returning:Swift.String
3644

3745
// CHECK: << remoteCall return: Echo: Caplin
3846
print("reply: \(reply)")
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 -disable-availability-checking -j2 -parse-as-library -I %t %s %S/../Inputs/FakeDistributedActorSystems.swift -o %t/a.out
4+
// RUN: %target-codesign %t/a.out
5+
// RUN: %target-run %t/a.out | %FileCheck %s --dump-input=always
6+
7+
// REQUIRES: executable_test
8+
// REQUIRES: concurrency
9+
// REQUIRES: distributed
10+
11+
// rdar://76038845
12+
// UNSUPPORTED: use_os_stdlib
13+
// UNSUPPORTED: back_deployment_runtime
14+
15+
// UNSUPPORTED: OS=windows-msvc
16+
17+
import Distributed
18+
import FakeDistributedActorSystems
19+
20+
typealias DefaultDistributedActorSystem = FakeRoundtripActorSystem
21+
22+
protocol KappaProtocol : DistributedActor where ActorSystem == FakeRoundtripActorSystem {
23+
distributed func echo(name: String) -> String
24+
}
25+
26+
distributed actor KappaProtocolImpl: KappaProtocol {
27+
// empty, gets default impl from extension on this actor
28+
}
29+
30+
extension KappaProtocolImpl {
31+
distributed func echo(name: String) -> String {
32+
return "Echo: \(name)"
33+
}
34+
}
35+
36+
func test() async throws {
37+
let system = DefaultDistributedActorSystem()
38+
39+
let local = KappaProtocolImpl(actorSystem: system)
40+
let ref = try KappaProtocolImpl.resolve(id: local.id, using: system)
41+
42+
let reply = try await ref.echo(name: "Caplin")
43+
// CHECK: >> remoteCall: on:main.KappaProtocolImpl, target:main.KappaProtocolImpl.echo(name:), invocation:FakeInvocationEncoder(genericSubs: [], arguments: ["Caplin"], returnType: Optional(Swift.String), errorType: nil), throwing:Swift.Never, returning:Swift.String
44+
45+
// CHECK: << remoteCall return: Echo: Caplin
46+
print("reply: \(reply)")
47+
// CHECK: reply: Echo: Caplin
48+
}
49+
50+
@main struct Main {
51+
static func main() async {
52+
try! await test()
53+
}
54+
}

0 commit comments

Comments
 (0)