diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Distributed.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Distributed.swift new file mode 100644 index 000000000..e703823a1 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Distributed.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Distributed +import SwiftJava + +public distributed actor DistributedHi { + public typealias ActorSystem = LocalTestingDistributedActorSystem + public distributed func hi() -> String { + "hi" + } +} + +public func makeDistributedHi() -> DistributedHi { + DistributedHi(actorSystem: LocalTestingDistributedActorSystem()) +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Isolated.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Isolated.swift index da045e56e..115f395e5 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Isolated.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Isolated.swift @@ -18,6 +18,22 @@ public actor Counter { var value: Int64 = 0 public init() {} + + public func incrementIsolated(by amount: Int64) -> Int64 { + value += amount + return value + } + + public nonisolated func label() -> String { + "Counter" + } + +} + +extension Counter { + public func currentValue() -> Int64 { + value + } } public func increment(_ counter: isolated Counter, by amount: Int64) -> Int64 { diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DistributedTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DistributedTest.java new file mode 100644 index 000000000..083bc63de --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DistributedTest.java @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.SwiftArena; + +import java.util.concurrent.Future; + +import static org.junit.jupiter.api.Assertions.*; + +public class DistributedTest { + + @Test + void distributedMethodReturnsFuture() throws Exception { + try (var arena = SwiftArena.ofConfined()) { + DistributedHi greeter = MySwiftLibrary.makeDistributedHi(arena); + + Future greeting = greeter.hi(); + assertEquals("hi", greeting.get()); + } + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/IsolatedTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/IsolatedTest.java index 9c6055685..3945b94f5 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/IsolatedTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/IsolatedTest.java @@ -56,4 +56,38 @@ void incrementThrows() throws Exception { assertEquals("swiftError", cause.getMessage()); } } + + @Test + void actorIsolatedMethodReturnsFuture() throws Exception { + try (var arena = SwiftArena.ofConfined()) { + Counter counter = Counter.init(arena); + + Future afterFirst = counter.incrementIsolated(3); + assertEquals(3, afterFirst.get()); + + Future afterSecond = counter.incrementIsolated(4); + assertEquals(7, afterSecond.get()); + } + } + + @Test + void actorIsolatedMethodInExtensionReturnsFuture() throws Exception { + try (var arena = SwiftArena.ofConfined()) { + Counter counter = Counter.init(arena); + counter.incrementIsolated(5).get(); + + Future current = counter.currentValue(); + assertEquals(5, current.get()); + } + } + + @Test + void nonisolatedMethodStaysSynchronous() { + try (var arena = SwiftArena.ofConfined()) { + Counter counter = Counter.init(arena); + + String label = counter.label(); + assertEquals("Counter", label); + } + } } diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index 38f11125c..55ea1fb77 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -136,6 +136,10 @@ struct CdeclLowering { throw LoweringError.isolatedParameterNotSupported() } + if signature.isImplicitlyAsync { + throw LoweringError.actorIsolatedMemberNotSupported() + } + // Lower the result. let loweredResult = try lowerResult(signature.result.type) @@ -1163,4 +1167,5 @@ enum LoweringError: Error { case unhandledType(SwiftType, file: String = #file, line: Int = #line) case effectNotSupported(SwiftEffectSpecifier, file: String = #file, line: Int = #line) case isolatedParameterNotSupported(file: String = #file, line: Int = #line) + case actorIsolatedMemberNotSupported(file: String = #file, line: Int = #line) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index fe2ff2a4e..f3e6e5189 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -216,7 +216,9 @@ extension JNISwift2JavaGenerator { } // Handle async methods and isolated methods - if decl.functionSignature.isAsync || decl.functionSignature.isIsolated { + if decl.functionSignature.isAsync || decl.functionSignature.isIsolated + || decl.functionSignature.isImplicitlyAsync + { self.convertToAsync( translatedFunctionSignature: &translatedFunctionSignature, nativeFunctionSignature: &nativeFunctionSignature, @@ -231,6 +233,7 @@ extension JNISwift2JavaGenerator { isThrowing: decl.isThrowing, isAsync: decl.isAsync, isIsolated: decl.isIsolated, + isImplicitlyAsync: decl.functionSignature.isImplicitlyAsync, nativeFunctionName: "$\(javaName)", parentName: parentName, functionTypes: funcTypes, @@ -689,7 +692,7 @@ extension JNISwift2JavaGenerator { nativeFunctionSignature.result.conversion = .asyncCompleteFuture( swiftFunctionResultType: originalFunctionSignature.result.type, nativeFunctionSignature: nativeFunctionSignature, - isThrowing: originalFunctionSignature.isThrowing, + isThrowing: originalFunctionSignature.isThrowing || originalFunctionSignature.isImplicitlyThrowing, completeMethodID: completeMethodID, completeExceptionallyMethodID: completeExceptionallyMethodID, ) @@ -1698,6 +1701,8 @@ extension JNISwift2JavaGenerator { var isIsolated: Bool + var isImplicitlyAsync: Bool + /// The name of the native function var nativeFunctionName: String @@ -1720,7 +1725,7 @@ extension JNISwift2JavaGenerator { func throwsClause() -> String { guard !translatedFunctionSignature.exceptions.isEmpty else { - return isThrowing && !(isAsync || isIsolated) ? " throws Exception" : "" + return isThrowing && !(isAsync || isIsolated || isImplicitlyAsync) ? " throws Exception" : "" } let signatureExceptions = translatedFunctionSignature.exceptions.compactMap(\.type.className).joined( diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index c0151622d..229512e24 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -772,7 +772,7 @@ extension JNISwift2JavaGenerator { } } - if decl.isThrowing, !(decl.isAsync || decl.isIsolated) { + if decl.isThrowing, !(decl.isAsync || decl.isIsolated || decl.isImplicitlyAsync) { printer.print("do {") printer.indent() printer.print(innerBody(in: &printer)) diff --git a/Sources/SwiftExtract/ExtractedDecls.swift b/Sources/SwiftExtract/ExtractedDecls.swift index 4a237d7c3..c0dac567e 100644 --- a/Sources/SwiftExtract/ExtractedDecls.swift +++ b/Sources/SwiftExtract/ExtractedDecls.swift @@ -369,13 +369,17 @@ public final class ExtractedFunc: ExtractedSwiftDecl, CustomStringConvertible { } public var isThrowing: Bool { - self.functionSignature.effectSpecifiers.contains(.throws) + self.functionSignature.effectSpecifiers.contains(.throws) || self.functionSignature.isImplicitlyThrowing } public var isAsync: Bool { self.functionSignature.isAsync } + public var isImplicitlyAsync: Bool { + self.functionSignature.isImplicitlyAsync + } + public var isIsolated: Bool { self.functionSignature.isIsolated } diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift index a7ad802c6..08201f5a7 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift @@ -31,14 +31,29 @@ public struct SwiftFunctionSignature: Equatable { public var genericParameters: [SwiftGenericParameterDeclaration] public var genericRequirements: [SwiftGenericRequirement] + public var isNonisolated: Bool = false + + public var isDistributed: Bool = false + public var isAsync: Bool { effectSpecifiers.contains(.async) } + public var isImplicitlyAsync: Bool { + guard !isAsync, !isNonisolated, case .instance(_, let selfType) = selfParameter else { + return false + } + return selfType.isActor + } + public var isThrowing: Bool { effectSpecifiers.contains(.throws) } + public var isImplicitlyThrowing: Bool { + !isThrowing && isDistributed + } + public var isTypedThrowing: Bool { thrownTypedError != nil } @@ -239,6 +254,15 @@ extension SwiftFunctionSignature { genericParameters: genericParams, genericRequirements: genericRequirements ) + + self.isNonisolated = + node.modifiers.contains { $0.name.tokenKind == .keyword(.nonisolated) } + || node.attributes.contains { attribute in + attribute.as(AttributeSyntax.self)? + .attributeName.as(IdentifierTypeSyntax.self)?.name.text == "concurrent" + } + + self.isDistributed = node.modifiers.contains { $0.name.tokenKind == .keyword(.distributed) } } public static func translateGenericParameters( diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftType.swift b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift index 0cb2fedf0..34f358353 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftType.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift @@ -77,6 +77,10 @@ public enum SwiftType: Equatable { asNominalType?.nominalTypeDecl } + public var isActor: Bool { + asNominalTypeDeclaration?.kind == .actor + } + /// True when this type is a synthetic placeholder produced by SwiftExtract /// for an unresolved name — see /// `SwiftNominalTypeDeclaration.isUnresolvedTypePlaceholder` for why these diff --git a/Tests/JExtractSwiftTests/JNI/JNIActorTests.swift b/Tests/JExtractSwiftTests/JNI/JNIActorTests.swift new file mode 100644 index 000000000..aca720352 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIActorTests.swift @@ -0,0 +1,275 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct JNIActor1MethodsTests { + + static let source = """ + public actor K { + public init() {} + public func hello() {} + public nonisolated func sync() {} + @concurrent public func concurrently() {} + @concurrent public func concurrentlyAsync() async {} + } + + extension K { + public func hi() {} + public nonisolated func syncInExtension() {} + } + """ + + @Test("Import: actor method is imported as a future (Java)") + func actorMethod_java() throws { + try assertOutput( + input: Self.source, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public java.util.concurrent.CompletableFuture hello() { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + K.$hello(this.$memoryAddress(), future$); + return future$.thenApply((futureResult$) -> { + return futureResult$; + } + ); + } + """, + """ + private static native void $hello(long selfPointer, java.util.concurrent.CompletableFuture result_future); + """, + ], + notExpectedChunks: [ + "public void hello() {", + "private static native void $hello(long selfPointer);", + ] + ) + } + + @Test("Import: actor method awaits the actor (Swift)") + func actorMethod_swift() throws { + try assertOutput( + input: Self.source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_K__00024hello__JLjava_util_concurrent_CompletableFuture_2") + ... + task = Task.immediate { + ... + await selfPointer$.pointee.hello() + """ + ] + ) + } + + @Test("Import: actor extension method is imported as a future (Java)") + func actorExtensionMethod_java() throws { + try assertOutput( + input: Self.source, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public java.util.concurrent.CompletableFuture hi() { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + K.$hi(this.$memoryAddress(), future$); + return future$.thenApply((futureResult$) -> { + return futureResult$; + } + ); + } + """, + """ + private static native void $hi(long selfPointer, java.util.concurrent.CompletableFuture result_future); + """, + ], + notExpectedChunks: [ + "public void hi() {", + "private static native void $hi(long selfPointer);", + ] + ) + } + + @Test("Import: actor extension method awaits the actor (Swift)") + func actorExtensionMethod_swift() throws { + try assertOutput( + input: Self.source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_K__00024hi__JLjava_util_concurrent_CompletableFuture_2") + ... + task = Task.immediate { + ... + await selfPointer$.pointee.hi() + """ + ] + ) + } + + @Test("Import: nonisolated actor method stays synchronous (Java)") + func nonisolatedMethod_java() throws { + try assertOutput( + input: Self.source, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public void sync() { + K.$sync(this.$memoryAddress()); + } + """, + """ + private static native void $sync(long selfPointer); + """, + """ + public void syncInExtension() { + K.$syncInExtension(this.$memoryAddress()); + } + """, + """ + private static native void $syncInExtension(long selfPointer); + """, + ], + notExpectedChunks: [ + "public java.util.concurrent.CompletableFuture sync() {", + "public java.util.concurrent.CompletableFuture syncInExtension() {", + ] + ) + } + + @Test("Import: nonisolated actor method does not await the actor (Swift)") + func nonisolatedMethod_swift() throws { + try assertOutput( + input: Self.source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_K__00024sync__J") + ... + selfPointer$.pointee.sync() + """ + ], + notExpectedChunks: [ + "await selfPointer$.pointee.sync()", + "await selfPointer$.pointee.syncInExtension()", + ] + ) + } + + @Test("Import: @concurrent actor method stays synchronous (Java)") + func concurrentMethod_java() throws { + try assertOutput( + input: Self.source, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public void concurrently() { + K.$concurrently(this.$memoryAddress()); + } + """, + """ + private static native void $concurrently(long selfPointer); + """, + ], + notExpectedChunks: [ + "public java.util.concurrent.CompletableFuture concurrently() {" + ] + ) + } + + @Test("Import: @concurrent actor method does not await the actor (Swift)") + func concurrentMethod_swift() throws { + try assertOutput( + input: Self.source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_K__00024concurrently__J") + ... + selfPointer$.pointee.concurrently() + """ + ], + notExpectedChunks: [ + "await selfPointer$.pointee.concurrently()" + ] + ) + } + + @Test("Import: @concurrent async actor method is imported as a future (Java)") + func concurrentAsyncMethod_java() throws { + try assertOutput( + input: Self.source, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public java.util.concurrent.CompletableFuture concurrentlyAsync() { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + K.$concurrentlyAsync(this.$memoryAddress(), future$); + return future$.thenApply((futureResult$) -> { + return futureResult$; + } + ); + } + """, + """ + private static native void $concurrentlyAsync(long selfPointer, java.util.concurrent.CompletableFuture result_future); + """, + ], + notExpectedChunks: [ + "public void concurrentlyAsync() {", + "private static native void $concurrentlyAsync(long selfPointer);", + ] + ) + } + + @Test("Import: @concurrent async actor method is awaited (Swift)") + func concurrentAsyncMethod_swift() throws { + try assertOutput( + input: Self.source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_K__00024concurrentlyAsync__JLjava_util_concurrent_CompletableFuture_2") + ... + task = Task.immediate { + ... + await selfPointer$.pointee.concurrentlyAsync() + """ + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIDistributedActorTests.swift b/Tests/JExtractSwiftTests/JNI/JNIDistributedActorTests.swift new file mode 100644 index 000000000..2ec0805b5 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIDistributedActorTests.swift @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct JNIDistributedActorTests { + + static let source = """ + public distributed actor D { + public distributed func hi() {} + } + """ + + @Test("Import distributed actor: distributed func is imported as a future (Java)") + func distributedFuncMethod_java() throws { + try assertOutput( + input: Self.source, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public java.util.concurrent.CompletableFuture hi() { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + D.$hi(this.$memoryAddress(), future$); + return future$.thenApply((futureResult$) -> { + return futureResult$; + } + ); + } + """, + """ + private static native void $hi(long selfPointer, java.util.concurrent.CompletableFuture result_future); + """, + ], + notExpectedChunks: [ + "public void hi() {", + "throws Exception", + ] + ) + } + + @Test("Import distributed actor: distributed func call is awaited with try (Swift)") + func distributedFuncMethod_swift() throws { + try assertOutput( + input: Self.source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_D__00024hi__JLjava_util_concurrent_CompletableFuture_2") + ... + task = Task.immediate { + ... + do { + ... + try await selfPointer$.pointee.hi() + """, + """ + catch { + let catchEnvironment = try! JavaVirtualMachine.shared().environment() + let exception = catchEnvironment.interface.NewObjectA(catchEnvironment, _JNIMethodIDCache.Exception.class, _JNIMethodIDCache.Exception.constructWithMessage, [String(describing: error).getJValue(in: catchEnvironment)]) + _ = catchEnvironment.interface.CallBooleanMethodA(catchEnvironment, globalFuture, _JNIMethodIDCache.CompletableFuture.completeExceptionally, [jvalue(l: exception)]) + } + """, + ] + ) + } +} diff --git a/Tests/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift index 0ac1f5507..426183ffb 100644 --- a/Tests/SwiftExtractTests/AnalysisResultTests.swift +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -711,4 +711,58 @@ struct AnalysisResultSuite { let count = try #require(fishTank.variables.first { $0.name == "count" }) #expect(count.isClass) } + + // ==== ----------------------------------------------------------------------- + // MARK: Actor members are extracted + @Test func actorMembersAreExtracted() throws { + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public actor K { + public init() {} + public func hello() {} + public nonisolated func explicitlyNonisolated() {} + @concurrent public func concurrently() -> Int { 0 } + @concurrent public func shouldBeAsync() async -> Int { 0 } + public static func staticMethod() {} + public distributed func distributedMethod() {} + } + + extension K { + public func hi() {} + } + """ + ) + ], + moduleName: "Aquarium" + ) + + let k = try #require(result.extractedTypes["K"]) + #expect(k.swiftNominal.kind == .actor) + + let hello = try #require(k.methods.first { $0.name == "hello" }) + #expect(hello.isImplicitlyAsync) + + let hi = try #require(k.methods.first { $0.name == "hi" }) + #expect(hi.isImplicitlyAsync) + + let explicitlyNonisolated = try #require(k.methods.first { $0.name == "explicitlyNonisolated" }) + #expect(!explicitlyNonisolated.isImplicitlyAsync) + + let concurrently = try #require(k.methods.first { $0.name == "concurrently" }) + #expect(!concurrently.isImplicitlyAsync) + + let shouldBeAsync = try #require(k.methods.first { $0.name == "shouldBeAsync" }) + #expect(shouldBeAsync.isAsync) + #expect(!shouldBeAsync.isImplicitlyAsync) + + let staticMethod = try #require(k.methods.first { $0.name == "staticMethod" }) + #expect(!staticMethod.isImplicitlyAsync) + + let distributedMethod = try #require(k.methods.first { $0.name == "distributedMethod" }) + #expect(distributedMethod.isImplicitlyAsync) + #expect(distributedMethod.isThrowing) + } }