Repository: swiftlang/swift
Pull request #80588: [Distributed] Reproducer for generics and library evolution mode
Author: login: ktoso
Labels: name: distributed
description: Feature → concurrency: distributed actor
Created: 2025-04-07T11:52:47Z | Merged: 2025-04-21T11:03:28Z

This corrects how we were dealing with dispatch thunks -- mostly be
removing a lot of special casing we did but doesn't seem necessary and
instead we correct and emit all the necessary information int TBD.

This builds on  https://github.com/swiftlang/swift/pull/74935 by further refining how we fixed that issue, and adds more regression tests. It also removes a load of special casing of distributed thunks in library evolution mode, which is great.

Resolves and adds regression test for for rdar://145292018

This is also a more proper fix to the previously resolved but in a not-great-way which caused other issues:
- refs rdar://128284016
- refs rdar://128310903


Diff:
diff --git a/lib/IRGen/GenMeta.cpp b/lib/IRGen/GenMeta.cpp
index 5b6c5854c9c11..62facbd61f240 100644
--- a/lib/IRGen/GenMeta.cpp
+++ b/lib/IRGen/GenMeta.cpp
@@ -1000,8 +1000,7 @@ namespace {
 
       // Emit the dispatch thunk.
       auto shouldEmitDispatchThunk =
-          (Resilient || IGM.getOptions().WitnessMethodElimination) &&
-          (!func.isDistributed() || !func.isDistributedThunk());
+          (Resilient || IGM.getOptions().WitnessMethodElimination);
       if (shouldEmitDispatchThunk) {
         IGM.emitDispatchThunk(func);
       }
@@ -1082,14 +1081,10 @@ namespace {
             // Define the method descriptor.
             SILDeclRef func(entry.getFunction());
 
-            /// Distributed thunks don't need method descriptors
-            if (!func.isDistributedThunk()) {
-              auto *descriptor =
-                B.getAddrOfCurrentPosition(
-                  IGM.ProtocolRequirementStructTy);
-              IGM.defineMethodDescriptor(func, Proto, descriptor,
-                                         IGM.ProtocolRequirementStructTy);
-            }
+            auto *descriptor =
+                B.getAddrOfCurrentPosition(IGM.ProtocolRequirementStructTy);
+            IGM.defineMethodDescriptor(func, Proto, descriptor,
+                                       IGM.ProtocolRequirementStructTy);
           }
         }
 
diff --git a/lib/IRGen/GenProto.cpp b/lib/IRGen/GenProto.cpp
index 695beeca36789..f9537053e984c 100644
--- a/lib/IRGen/GenProto.cpp
+++ b/lib/IRGen/GenProto.cpp
@@ -2348,12 +2348,6 @@ namespace {
               LinkEntity::forBaseConformanceDescriptor(requirement));
           B.addRelativeAddress(baseConformanceDescriptor);
         } else if (entry.getKind() == SILWitnessTable::Method) {
-          // distributed thunks don't need resilience
-          if (entry.getMethodWitness().Requirement.isDistributedThunk()) {
-            witnesses = witnesses.drop_back();
-            continue;
-          }
-
           // Method descriptor.
           auto declRef = entry.getMethodWitness().Requirement;
           auto requirement =
diff --git a/lib/IRGen/IRSymbolVisitor.cpp b/lib/IRGen/IRSymbolVisitor.cpp
index 7134aa8752b9f..b62d906146aa4 100644
--- a/lib/IRGen/IRSymbolVisitor.cpp
+++ b/lib/IRGen/IRSymbolVisitor.cpp
@@ -157,14 +157,6 @@ class IRSymbolVisitorImpl : public SILSymbolVisitor {
   }
 
   void addMethodDescriptor(SILDeclRef declRef) override {
-    if (declRef.isDistributedThunk()) {
-      auto afd = declRef.getAbstractFunctionDecl();
-      auto DC = afd->getDeclContext();
-      if (isa<ProtocolDecl>(DC)) {
-        return;
-      }
-    }
-
     addLinkEntity(LinkEntity::forMethodDescriptor(declRef));
   }
 
diff --git a/lib/SIL/IR/SILSymbolVisitor.cpp b/lib/SIL/IR/SILSymbolVisitor.cpp
index be6078d41eade..3da9722583e55 100644
--- a/lib/SIL/IR/SILSymbolVisitor.cpp
+++ b/lib/SIL/IR/SILSymbolVisitor.cpp
@@ -835,7 +835,6 @@ class SILSymbolVisitorImpl : public ASTVisitor<SILSymbolVisitorImpl> {
                   V.Ctx.getOpts().WitnessMethodElimination} {}
 
         void addMethod(SILDeclRef declRef) {
-          // TODO: alternatively maybe prevent adding distributed thunk here rather than inside those?
           if (Resilient || WitnessMethodElimination) {
             Visitor.addDispatchThunk(declRef);
             Visitor.addMethodDescriptor(declRef);
diff --git a/test/Distributed/Runtime/distributed_actor_library_evolution_da_protocol_use.swift b/test/Distributed/Runtime/distributed_actor_library_evolution_da_protocol_use.swift
new file mode 100644
index 0000000000000..e4f65597d2420
--- /dev/null
+++ b/test/Distributed/Runtime/distributed_actor_library_evolution_da_protocol_use.swift
@@ -0,0 +1,54 @@
+// REQUIRES: VENDOR=apple
+// REQUIRES: OS=macosx || OS=linux-gnu
+// REQUIRES: concurrency
+// REQUIRES: distributed
+// UNSUPPORTED: use_os_stdlib
+
+// RUN: %empty-directory(%t)
+// RUN: split-file %s %t
+
+// RUN: %target-build-swift -Xfrontend -validate-tbd-against-ir=all -enable-library-evolution -target %target-cpu-apple-macosx13.0 -parse-as-library -emit-library -emit-module-path %t/Library.swiftmodule -module-name Library %t/library.swift -o %t/%target-library-name(Library)
+// RUN: %target-build-swift -Xfrontend -validate-tbd-against-ir=all -target %target-cpu-apple-macosx13.0 -parse-as-library -lLibrary -module-name main -I %t -L %t %t/main.swift -o %t/a.out
+
+// RUN: %target-codesign %t/a.out
+// RUN: %target-run %t/a.out | %FileCheck %s
+
+//--- library.swift
+import Distributed
+
+public protocol SimpleProtocol: DistributedActor
+    where ActorSystem == LocalTestingDistributedActorSystem {
+
+  // nonisolated override var id: ID { get } // comes from DistributedActor
+
+  // Has to have a distributed method to fail
+  distributed func test() -> Int
+}
+
+//--- main.swift
+import Distributed
+import Library
+
+public distributed actor SimpleActor: SimpleProtocol {
+  public distributed func test() -> Int {
+    print("SimpleActor.test")
+    return 1
+  }
+}
+
+public func makeFromFail<Act: SimpleProtocol>(_ act: Act) async {
+  print(act.id)
+  try! await print("act.test() = \(act.test())")
+  // CHECK: SimpleActor.test
+  // CHECK: act.test() = 1
+}
+
+@main
+struct TestSwiftFrameworkTests {
+  static func main() async {
+    let system = LocalTestingDistributedActorSystem()
+
+    let simpleActor = SimpleActor(actorSystem: system)
+    await makeFromFail(simpleActor)
+  }
+}
\ No newline at end of file


Reviews:
user: login: xedin
body: LGTM! I left a few comments about the test inline but nothing blocking.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T07:21:05Z
user: login: ktoso
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T13:05:46Z
user: login: ktoso
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T13:06:13Z

Review comments:
diff_hunk: @@ -0,0 +1,66 @@
+// REQUIRES: VENDOR=apple
+// REQUIRES: concurrency
+// REQUIRES: distributed
+
+// RUN: %empty-directory(%t)
+// RUN: split-file %s %t
+
+// RUN: %target-build-swift -Xfrontend -validate-tbd-against-ir=all -enable-library-evolution -target %target-cpu-apple-macosx13.0 -parse-as-library -emit-library -emit-module-path %t/Library.swiftmodule -module-name Library %t/library.swift -o %t/%target-library-name(Library)
+// RUN: %target-build-swift -Xfrontend -validate-tbd-against-ir=all -target %target-cpu-apple-macosx13.0 -parse-as-library -lLibrary -module-name main -I %t -L %t %t/main.swift -o %t/a.out
+
+
+// RUN: %target-codesign %t/a.out
+// RUN: %target-run %t/a.out
+
+//--- library.swift
+import Distributed
+
+//public protocol NormalProtocol {
+//  func NORMAL() async -> Int
+//}
+
+public protocol SimpleProtocol: DistributedActor
+    where ActorSystem == LocalTestingDistributedActorSystem {
+
+  // nonisolated override var id: ID { get } // comes from DistributedActor
+
+  // Has to have a distributed method to fail
+  distributed func test() -> Int
+}
+
+//--- main.swift
+import Distributed
+import Library
+
+//actor NormalActor: NormalProtocol {
+//  func NORMAL() async -> Int { 1 }
+//}
+
+public distributed actor SimpleActor: SimpleProtocol {
+  public distributed func test() -> Int { 1 }
+}
+
+// Passes
+public func makeFromPass<Act: DistributedActor>(_ act: Act) {
+  print(act.id)
+}
+
+// Fails
+public func makeFromFail<Act: SimpleProtocol>(_ act: Act) async {
+  print(act.id)
+  try! await print(act.test())
+}
+
+@main
+struct TestSwiftFrameworkTests {
+  static func main() async {
+    let system = LocalTestingDistributedActorSystem()
+
+    // let norm = NormalActor()
+
+    let simpleActor = SimpleActor(actorSystem: system)
+//    makeFromPass(simpleActor)
+
+    await makeFromFail(simpleActor)
path: test/Distributed/Runtime/distributed_actor_library_evolution_da_protocol_use.swift
user: login: xedin
body: Let's add a file check to make sure that the output is reasonable and try to use `test()` as well to make sure that distributed methods work through dispatch thunks as expected.
created_at: 2025-04-18T07:20:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+// REQUIRES: VENDOR=apple
+// REQUIRES: concurrency
+// REQUIRES: distributed
+
+// RUN: %empty-directory(%t)
+// RUN: split-file %s %t
+
+// RUN: %target-build-swift -Xfrontend -validate-tbd-against-ir=all -enable-library-evolution -target %target-cpu-apple-macosx13.0 -parse-as-library -emit-library -emit-module-path %t/Library.swiftmodule -module-name Library %t/library.swift -o %t/%target-library-name(Library)
+// RUN: %target-build-swift -Xfrontend -validate-tbd-against-ir=all -target %target-cpu-apple-macosx13.0 -parse-as-library -lLibrary -module-name main -I %t -L %t %t/main.swift -o %t/a.out
+
+
+// RUN: %target-codesign %t/a.out
+// RUN: %target-run %t/a.out
+
+//--- library.swift
+import Distributed
+
+//public protocol NormalProtocol {
+//  func NORMAL() async -> Int
+//}
+
+public protocol SimpleProtocol: DistributedActor
+    where ActorSystem == LocalTestingDistributedActorSystem {
+
+  // nonisolated override var id: ID { get } // comes from DistributedActor
+
+  // Has to have a distributed method to fail
+  distributed func test() -> Int
+}
+
+//--- main.swift
+import Distributed
+import Library
+
+//actor NormalActor: NormalProtocol {
+//  func NORMAL() async -> Int { 1 }
+//}
+
+public distributed actor SimpleActor: SimpleProtocol {
+  public distributed func test() -> Int { 1 }
+}
+
+// Passes
+public func makeFromPass<Act: DistributedActor>(_ act: Act) {
+  print(act.id)
+}
+
+// Fails
+public func makeFromFail<Act: SimpleProtocol>(_ act: Act) async {
+  print(act.id)
+  try! await print(act.test())
+}
+
+@main
+struct TestSwiftFrameworkTests {
+  static func main() async {
+    let system = LocalTestingDistributedActorSystem()
+
+    // let norm = NormalActor()
+
+    let simpleActor = SimpleActor(actorSystem: system)
+//    makeFromPass(simpleActor)
path: test/Distributed/Runtime/distributed_actor_library_evolution_da_protocol_use.swift
user: login: xedin
body: If it's not used, let's just remove it and a declaration itself.
created_at: 2025-04-18T07:20:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+// REQUIRES: VENDOR=apple
+// REQUIRES: concurrency
+// REQUIRES: distributed
+
+// RUN: %empty-directory(%t)
+// RUN: split-file %s %t
+
+// RUN: %target-build-swift -Xfrontend -validate-tbd-against-ir=all -enable-library-evolution -target %target-cpu-apple-macosx13.0 -parse-as-library -emit-library -emit-module-path %t/Library.swiftmodule -module-name Library %t/library.swift -o %t/%target-library-name(Library)
+// RUN: %target-build-swift -Xfrontend -validate-tbd-against-ir=all -target %target-cpu-apple-macosx13.0 -parse-as-library -lLibrary -module-name main -I %t -L %t %t/main.swift -o %t/a.out
+
+
+// RUN: %target-codesign %t/a.out
+// RUN: %target-run %t/a.out
+
+//--- library.swift
+import Distributed
+
+//public protocol NormalProtocol {
+//  func NORMAL() async -> Int
+//}
+
+public protocol SimpleProtocol: DistributedActor
+    where ActorSystem == LocalTestingDistributedActorSystem {
+
+  // nonisolated override var id: ID { get } // comes from DistributedActor
+
+  // Has to have a distributed method to fail
+  distributed func test() -> Int
+}
+
+//--- main.swift
+import Distributed
+import Library
+
+//actor NormalActor: NormalProtocol {
+//  func NORMAL() async -> Int { 1 }
+//}
+
+public distributed actor SimpleActor: SimpleProtocol {
+  public distributed func test() -> Int { 1 }
+}
+
+// Passes
+public func makeFromPass<Act: DistributedActor>(_ act: Act) {
+  print(act.id)
+}
+
+// Fails
+public func makeFromFail<Act: SimpleProtocol>(_ act: Act) async {
+  print(act.id)
+  try! await print(act.test())
+}
+
+@main
+struct TestSwiftFrameworkTests {
+  static func main() async {
+    let system = LocalTestingDistributedActorSystem()
+
+    // let norm = NormalActor()
+
+    let simpleActor = SimpleActor(actorSystem: system)
+//    makeFromPass(simpleActor)
path: test/Distributed/Runtime/distributed_actor_library_evolution_da_protocol_use.swift
user: login: ktoso
body: Yep ok
created_at: 2025-04-18T13:05:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+// REQUIRES: VENDOR=apple
+// REQUIRES: concurrency
+// REQUIRES: distributed
+
+// RUN: %empty-directory(%t)
+// RUN: split-file %s %t
+
+// RUN: %target-build-swift -Xfrontend -validate-tbd-against-ir=all -enable-library-evolution -target %target-cpu-apple-macosx13.0 -parse-as-library -emit-library -emit-module-path %t/Library.swiftmodule -module-name Library %t/library.swift -o %t/%target-library-name(Library)
+// RUN: %target-build-swift -Xfrontend -validate-tbd-against-ir=all -target %target-cpu-apple-macosx13.0 -parse-as-library -lLibrary -module-name main -I %t -L %t %t/main.swift -o %t/a.out
+
+
+// RUN: %target-codesign %t/a.out
+// RUN: %target-run %t/a.out
+
+//--- library.swift
+import Distributed
+
+//public protocol NormalProtocol {
+//  func NORMAL() async -> Int
+//}
+
+public protocol SimpleProtocol: DistributedActor
+    where ActorSystem == LocalTestingDistributedActorSystem {
+
+  // nonisolated override var id: ID { get } // comes from DistributedActor
+
+  // Has to have a distributed method to fail
+  distributed func test() -> Int
+}
+
+//--- main.swift
+import Distributed
+import Library
+
+//actor NormalActor: NormalProtocol {
+//  func NORMAL() async -> Int { 1 }
+//}
+
+public distributed actor SimpleActor: SimpleProtocol {
+  public distributed func test() -> Int { 1 }
+}
+
+// Passes
+public func makeFromPass<Act: DistributedActor>(_ act: Act) {
+  print(act.id)
+}
+
+// Fails
+public func makeFromFail<Act: SimpleProtocol>(_ act: Act) async {
+  print(act.id)
+  try! await print(act.test())
+}
+
+@main
+struct TestSwiftFrameworkTests {
+  static func main() async {
+    let system = LocalTestingDistributedActorSystem()
+
+    // let norm = NormalActor()
+
+    let simpleActor = SimpleActor(actorSystem: system)
+//    makeFromPass(simpleActor)
+
+    await makeFromFail(simpleActor)
path: test/Distributed/Runtime/distributed_actor_library_evolution_da_protocol_use.swift
user: login: ktoso
body: Good point, I should make sure the thunks get called as expected as well in remote case. They should, but hey, let's check 
created_at: 2025-04-18T13:06:13Z
author_association: CONTRIBUTOR

PR comments:
user: login: ktoso
created_at: 2025-04-18T02:22:44Z
body: Figured out a solution and even fixed other TBD issues along the way, and past TBD issues in a cleaner way. Will want to verify this in a project as well to make sure we don't break anyone with this but it looks solid -- way less special casing!
author_association: CONTRIBUTOR
user: login: ktoso
created_at: 2025-04-18T02:23:04Z
body: @swift-ci please smoke test
author_association: CONTRIBUTOR
user: login: ktoso
created_at: 2025-04-18T02:23:09Z
body: @swift-ci please test
author_association: CONTRIBUTOR
user: login: ktoso
created_at: 2025-04-18T02:25:45Z
body: @swift-ci please build toolchain macOS
author_association: CONTRIBUTOR
user: login: xedin
created_at: 2025-04-18T04:13:42Z
body: @swift-ci please test source compatibility
author_association: CONTRIBUTOR
user: login: ktoso
created_at: 2025-04-21T02:58:54Z
body: Hmm, build issue on simulator run, let me restrict this to macos and linux


> <unknown>:0: warning: using sysroot for 'iPhoneSimulator' but targeting 'MacOSX'
> <unknown>:0: warning: using sysroot for 'iPhoneSimulator' but targeting 'MacOSX'
> ld: building for 'macOS', but linking in dylib (/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib/libobjc.A.tbd) built for 'iOS-simulator'
<unknown>:0: error: link command failed with exit code 1 (use -v to see invocation)
author_association: CONTRIBUTOR
user: login: ktoso
created_at: 2025-04-21T03:03:22Z
body: @swift-ci please test
author_association: CONTRIBUTOR