From eb3337d1295e7df80645bbe45646c54879938ae0 Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Wed, 19 Aug 2026 10:41:47 +0300 Subject: [PATCH 01/11] Recognize Actor methods and methods in extensions as `implicitly async` to handle them as async during conversion. --- Sources/SwiftExtract/ExtractedDecls.swift | 4 +++ .../SwiftTypes/SwiftFunctionSignature.swift | 4 +++ .../SwiftExtract/SwiftTypes/SwiftType.swift | 4 +++ .../AnalysisResultTests.swift | 33 +++++++++++++++++++ 4 files changed, 45 insertions(+) diff --git a/Sources/SwiftExtract/ExtractedDecls.swift b/Sources/SwiftExtract/ExtractedDecls.swift index 4a237d7c3..4601106be 100644 --- a/Sources/SwiftExtract/ExtractedDecls.swift +++ b/Sources/SwiftExtract/ExtractedDecls.swift @@ -376,6 +376,10 @@ public final class ExtractedFunc: ExtractedSwiftDecl, CustomStringConvertible { 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..359bdcd13 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift @@ -35,6 +35,10 @@ public struct SwiftFunctionSignature: Equatable { effectSpecifiers.contains(.async) } + public var isImplicitlyAsync: Bool { + return !isAsync && selfParameter?.selfType.isActor == true + } + public var isThrowing: Bool { effectSpecifiers.contains(.throws) } diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftType.swift b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift index 0cb2fedf0..22edfde67 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/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift index 0ac1f5507..a865b0e8a 100644 --- a/Tests/SwiftExtractTests/AnalysisResultTests.swift +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -712,3 +712,36 @@ struct AnalysisResultSuite { #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() {} + } + + 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) + } +} \ No newline at end of file From af8fd89a8b69cd07591c6cf5dbc208351b94f190 Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Wed, 19 Aug 2026 11:09:02 +0300 Subject: [PATCH 02/11] Recognize actor methods as Async functions in JNI translation --- ...ISwift2JavaGenerator+JavaTranslation.swift | 8 +- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 2 +- .../JNI/JNIActorTests.swift | 127 ++++++++++++++++++ .../AnalysisResultTests.swift | 4 +- 4 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 Tests/JExtractSwiftTests/JNI/JNIActorTests.swift diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index fe2ff2a4e..f9d489e45 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -216,7 +216,8 @@ 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 +232,7 @@ extension JNISwift2JavaGenerator { isThrowing: decl.isThrowing, isAsync: decl.isAsync, isIsolated: decl.isIsolated, + isImplicitlyAsync: decl.functionSignature.isImplicitlyAsync, nativeFunctionName: "$\(javaName)", parentName: parentName, functionTypes: funcTypes, @@ -1698,6 +1700,8 @@ extension JNISwift2JavaGenerator { var isIsolated: Bool + var isImplicitlyAsync: Bool + /// The name of the native function var nativeFunctionName: String @@ -1720,7 +1724,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/Tests/JExtractSwiftTests/JNI/JNIActorTests.swift b/Tests/JExtractSwiftTests/JNI/JNIActorTests.swift new file mode 100644 index 000000000..ab0de8d0c --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIActorTests.swift @@ -0,0 +1,127 @@ +//===----------------------------------------------------------------------===// +// +// 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() {} + } + + extension K { + public func hi() {} + } + """ + + @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() + """ + ] + ) + } +} diff --git a/Tests/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift index a865b0e8a..c2679f997 100644 --- a/Tests/SwiftExtractTests/AnalysisResultTests.swift +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -711,7 +711,7 @@ struct AnalysisResultSuite { let count = try #require(fishTank.variables.first { $0.name == "count" }) #expect(count.isClass) } -} + // ==== ----------------------------------------------------------------------- // MARK: Actor members are extracted @@ -744,4 +744,4 @@ struct AnalysisResultSuite { let hi = try #require(k.methods.first { $0.name == "hi" }) #expect(hi.isImplicitlyAsync) } -} \ No newline at end of file +} From 3929e4aa07acbcf5467dd9a54d3ec23d4d67af2c Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Wed, 19 Aug 2026 15:59:45 +0300 Subject: [PATCH 03/11] Apply swift format --- .../JNI/JNISwift2JavaGenerator+JavaTranslation.swift | 3 ++- Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift | 2 +- Sources/SwiftExtract/SwiftTypes/SwiftType.swift | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index f9d489e45..1271ebc21 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -217,7 +217,8 @@ extension JNISwift2JavaGenerator { // Handle async methods and isolated methods if decl.functionSignature.isAsync || decl.functionSignature.isIsolated - || decl.functionSignature.isImplicitlyAsync { + || decl.functionSignature.isImplicitlyAsync + { self.convertToAsync( translatedFunctionSignature: &translatedFunctionSignature, nativeFunctionSignature: &nativeFunctionSignature, diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift index 359bdcd13..4a5e1ecaf 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift @@ -36,7 +36,7 @@ public struct SwiftFunctionSignature: Equatable { } public var isImplicitlyAsync: Bool { - return !isAsync && selfParameter?.selfType.isActor == true + !isAsync && selfParameter?.selfType.isActor == true } public var isThrowing: Bool { diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftType.swift b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift index 22edfde67..34f358353 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftType.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftType.swift @@ -77,7 +77,7 @@ public enum SwiftType: Equatable { asNominalType?.nominalTypeDecl } - public var isActor : Bool { + public var isActor: Bool { asNominalTypeDeclaration?.kind == .actor } From 4ebd90bd992063596c1cf8dfb9af13ac25ab0e3c Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Wed, 19 Aug 2026 17:17:49 +0300 Subject: [PATCH 04/11] Guard `isImplicitlyAsync` from static and class methods and initializers --- Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift | 5 ++++- Tests/SwiftExtractTests/AnalysisResultTests.swift | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift index 4a5e1ecaf..8a3517406 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift @@ -36,7 +36,10 @@ public struct SwiftFunctionSignature: Equatable { } public var isImplicitlyAsync: Bool { - !isAsync && selfParameter?.selfType.isActor == true + guard !isAsync, case .instance(_, let selfType) = selfParameter else { + return false + } + return selfType.isActor } public var isThrowing: Bool { diff --git a/Tests/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift index c2679f997..cad3127b6 100644 --- a/Tests/SwiftExtractTests/AnalysisResultTests.swift +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -712,7 +712,6 @@ struct AnalysisResultSuite { #expect(count.isClass) } - // ==== ----------------------------------------------------------------------- // MARK: Actor members are extracted @Test func actorMembersAreExtracted() throws { From 65202e4efef3ac4c9110e3a12cbdba38c45274f1 Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Thu, 20 Aug 2026 10:09:22 +0300 Subject: [PATCH 05/11] Recognise nonisolated methods --- .../SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift | 5 ++++- Tests/SwiftExtractTests/AnalysisResultTests.swift | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift index 8a3517406..06d9ea14c 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift @@ -31,12 +31,14 @@ public struct SwiftFunctionSignature: Equatable { public var genericParameters: [SwiftGenericParameterDeclaration] public var genericRequirements: [SwiftGenericRequirement] + public var isNonisolated: Bool = false + public var isAsync: Bool { effectSpecifiers.contains(.async) } public var isImplicitlyAsync: Bool { - guard !isAsync, case .instance(_, let selfType) = selfParameter else { + guard !isAsync, !isNonisolated, case .instance(_, let selfType) = selfParameter else { return false } return selfType.isActor @@ -246,6 +248,7 @@ extension SwiftFunctionSignature { genericParameters: genericParams, genericRequirements: genericRequirements ) + self.isNonisolated = node.modifiers.contains { $0.name.tokenKind == .keyword(.nonisolated) } } public static func translateGenericParameters( diff --git a/Tests/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift index cad3127b6..94441ebc9 100644 --- a/Tests/SwiftExtractTests/AnalysisResultTests.swift +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -723,6 +723,8 @@ struct AnalysisResultSuite { public actor K { public init() {} public func hello() {} + public nonisolated func explicitlyNonisolated() {} + public static func staticMethod() {} } extension K { @@ -742,5 +744,11 @@ struct AnalysisResultSuite { 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 staticMethod = try #require(k.methods.first { $0.name == "staticMethod" }) + #expect(!staticMethod.isImplicitlyAsync) } } From 1c42f1ec515a14f1d8eac5b207d208559c19f2c4 Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Thu, 20 Aug 2026 10:54:36 +0300 Subject: [PATCH 06/11] Recognise `@concurrent` as non isolated func --- .../SwiftTypes/SwiftFunctionSignature.swift | 8 ++- .../JNI/JNIActorTests.swift | 55 +++++++++++++++++++ .../AnalysisResultTests.swift | 4 ++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift index 06d9ea14c..15416d0de 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift @@ -248,7 +248,13 @@ extension SwiftFunctionSignature { genericParameters: genericParams, genericRequirements: genericRequirements ) - self.isNonisolated = node.modifiers.contains { $0.name.tokenKind == .keyword(.nonisolated) } + + 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" + } } public static func translateGenericParameters( diff --git a/Tests/JExtractSwiftTests/JNI/JNIActorTests.swift b/Tests/JExtractSwiftTests/JNI/JNIActorTests.swift index ab0de8d0c..19bc9138b 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIActorTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIActorTests.swift @@ -22,10 +22,12 @@ struct JNIActor1MethodsTests { public actor K { public init() {} public func hello() {} + public nonisolated func sync() {} } extension K { public func hi() {} + public nonisolated func syncInExtension() {} } """ @@ -124,4 +126,57 @@ struct JNIActor1MethodsTests { ] ) } + + @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()", + ] + ) + } } diff --git a/Tests/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift index 94441ebc9..29efeacb7 100644 --- a/Tests/SwiftExtractTests/AnalysisResultTests.swift +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -724,6 +724,7 @@ struct AnalysisResultSuite { public init() {} public func hello() {} public nonisolated func explicitlyNonisolated() {} + @concurrent public func concurrently() -> Int { 0 } public static func staticMethod() {} } @@ -748,6 +749,9 @@ struct AnalysisResultSuite { 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 staticMethod = try #require(k.methods.first { $0.name == "staticMethod" }) #expect(!staticMethod.isImplicitlyAsync) } From 6df12ebb320b04d3d98751dc6f1aabe0531bed9f Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Thu, 20 Aug 2026 11:12:23 +0300 Subject: [PATCH 07/11] Handle `@concurrent` inside actors --- .../JNI/JNIActorTests.swift | 93 +++++++++++++++++++ .../AnalysisResultTests.swift | 5 + 2 files changed, 98 insertions(+) diff --git a/Tests/JExtractSwiftTests/JNI/JNIActorTests.swift b/Tests/JExtractSwiftTests/JNI/JNIActorTests.swift index 19bc9138b..aca720352 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIActorTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIActorTests.swift @@ -23,6 +23,8 @@ struct JNIActor1MethodsTests { public init() {} public func hello() {} public nonisolated func sync() {} + @concurrent public func concurrently() {} + @concurrent public func concurrentlyAsync() async {} } extension K { @@ -179,4 +181,95 @@ struct JNIActor1MethodsTests { ] ) } + + @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/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift index 29efeacb7..6675718d7 100644 --- a/Tests/SwiftExtractTests/AnalysisResultTests.swift +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -725,6 +725,7 @@ struct AnalysisResultSuite { public func hello() {} public nonisolated func explicitlyNonisolated() {} @concurrent public func concurrently() -> Int { 0 } + @concurrent public func shouldBeAsync() async -> Int { 0 } public static func staticMethod() {} } @@ -752,6 +753,10 @@ struct AnalysisResultSuite { 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) } From 3c3bd0275663eef9567f7e744996abc2ff022890 Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Thu, 20 Aug 2026 11:56:12 +0300 Subject: [PATCH 08/11] Add runtime tests --- .../Sources/MySwiftLibrary/Isolated.swift | 16 +++++++++ .../java/com/example/swift/IsolatedTest.java | 34 +++++++++++++++++++ 2 files changed, 50 insertions(+) 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/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); + } + } } From eeb0c3728caf859e5cf0ce2dc2eb8f3622c76567 Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Thu, 20 Aug 2026 11:56:40 +0300 Subject: [PATCH 09/11] Throw lowering error in `FFM` mode --- .../FFMSwift2JavaGenerator+FunctionLowering.swift | 5 +++++ 1 file changed, 5 insertions(+) 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) } From e1c5463694e5ba600905cd0ff4aa0542cee6d835 Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Thu, 20 Aug 2026 17:52:20 +0300 Subject: [PATCH 10/11] Recognize `distributed` methods as throwing --- ...ISwift2JavaGenerator+JavaTranslation.swift | 2 +- Sources/SwiftExtract/ExtractedDecls.swift | 2 +- .../SwiftTypes/SwiftFunctionSignature.swift | 8 ++ .../JNI/JNIDistributedActorTests.swift | 83 +++++++++++++++++++ .../AnalysisResultTests.swift | 5 ++ 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 Tests/JExtractSwiftTests/JNI/JNIDistributedActorTests.swift diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 1271ebc21..f3e6e5189 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -692,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, ) diff --git a/Sources/SwiftExtract/ExtractedDecls.swift b/Sources/SwiftExtract/ExtractedDecls.swift index 4601106be..c0dac567e 100644 --- a/Sources/SwiftExtract/ExtractedDecls.swift +++ b/Sources/SwiftExtract/ExtractedDecls.swift @@ -369,7 +369,7 @@ 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 { diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift index 15416d0de..08201f5a7 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift @@ -33,6 +33,8 @@ public struct SwiftFunctionSignature: Equatable { public var isNonisolated: Bool = false + public var isDistributed: Bool = false + public var isAsync: Bool { effectSpecifiers.contains(.async) } @@ -48,6 +50,10 @@ public struct SwiftFunctionSignature: Equatable { effectSpecifiers.contains(.throws) } + public var isImplicitlyThrowing: Bool { + !isThrowing && isDistributed + } + public var isTypedThrowing: Bool { thrownTypedError != nil } @@ -255,6 +261,8 @@ extension SwiftFunctionSignature { 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/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 6675718d7..426183ffb 100644 --- a/Tests/SwiftExtractTests/AnalysisResultTests.swift +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -727,6 +727,7 @@ struct AnalysisResultSuite { @concurrent public func concurrently() -> Int { 0 } @concurrent public func shouldBeAsync() async -> Int { 0 } public static func staticMethod() {} + public distributed func distributedMethod() {} } extension K { @@ -759,5 +760,9 @@ struct AnalysisResultSuite { 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) } } From 059552f6a029026b6dd313accaa70f40532054ce Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Fri, 21 Aug 2026 10:40:13 +0300 Subject: [PATCH 11/11] Add runtime test for `Distributed` methods --- .../Sources/MySwiftLibrary/Distributed.swift | 27 ++++++++++++++ .../com/example/swift/DistributedTest.java | 35 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Distributed.swift create mode 100644 Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DistributedTest.java 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/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()); + } + } +}