From 4878473b6093b4a14a3dc7086a989af76baae3ce Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Fri, 14 Aug 2026 04:04:21 +0300 Subject: [PATCH 1/5] Extract functions with 'isolated' parameters --- .../SwiftTypes/SwiftParameter.swift | 11 +++++++- .../AnalysisResultTests.swift | 26 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift b/Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift index 71db093df..896449e0a 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift @@ -26,6 +26,8 @@ public struct SwiftParameter: Equatable { public var hasDefaultValue: Bool /// The default-value expression source, if any (e.g. `42`, `[]`). public var defaultValueExpression: String? + /// Whether the parameter is marked `isolated`. + public var isIsolated: Bool public init( convention: SwiftParameterConvention, @@ -34,7 +36,8 @@ public struct SwiftParameter: Equatable { type: SwiftType, isVariadic: Bool = false, hasDefaultValue: Bool = false, - defaultValueExpression: String? = nil + defaultValueExpression: String? = nil, + isIsolated: Bool = false ) { self.convention = convention self.argumentLabel = argumentLabel @@ -43,6 +46,7 @@ public struct SwiftParameter: Equatable { self.isVariadic = isVariadic self.hasDefaultValue = hasDefaultValue self.defaultValueExpression = defaultValueExpression + self.isIsolated = isIsolated } /// The simple parameter name, falling back to the argument label. @@ -106,6 +110,7 @@ extension SwiftParameter { self.isVariadic = false self.hasDefaultValue = node.defaultValue != nil self.defaultValueExpression = node.defaultValue?.value.trimmedDescription + self.isIsolated = false } } @@ -115,6 +120,7 @@ extension SwiftParameter { // specifiers on the type for other conventions (like `inout`). var type = node.type var convention = SwiftParameterConvention.byValue + var isIsolated = false if let attributedType = type.as(AttributedTypeSyntax.self) { var sawUnknownSpecifier = false for specifier in attributedType.specifiers { @@ -128,6 +134,8 @@ extension SwiftParameter { convention = .consuming case .keyword(.inout): convention = .inout + case .keyword(.isolated): + isIsolated = true default: sawUnknownSpecifier = true break @@ -140,6 +148,7 @@ extension SwiftParameter { } } self.convention = convention + self.isIsolated = isIsolated // Determine the type. self.type = try SwiftType(type, lookupContext: lookupContext) diff --git a/Tests/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift index 26715ecad..e5371ef34 100644 --- a/Tests/SwiftExtractTests/AnalysisResultTests.swift +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -543,4 +543,30 @@ struct AnalysisResultSuite { #expect(result.extractedTypes["AlwaysHere"] != nil) #expect(result.extractedTypes["OnlyWhenImportable"] != nil) } + + // ==== ----------------------------------------------------------------------- + // MARK: Method with an isolated parameter is extracted + @Test func methodWithIsolatedParameterIsExtracted() throws { + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + public actor MyActor {} + public class Service { + public init() {} + public func run(_ a: isolated MyActor) {} + } + """ + ) + ], + moduleName: "Aquarium" + ) + + let service = try #require(result.extractedTypes["Service"]) + let run = try #require(service.methods.first { $0.name == "run" }) + let param = try #require(run.functionSignature.parameters.first) + + #expect(param.isIsolated) + } } From aab6bb7495611d05ef90509ca1616db413735b52 Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Fri, 14 Aug 2026 06:45:41 +0300 Subject: [PATCH 2/5] JNI: import functions with 'isolated' parameters as async --- ...ISwift2JavaGenerator+JavaTranslation.swift | 15 +++- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 2 +- Sources/SwiftExtract/ExtractedDecls.swift | 4 + .../SwiftTypes/SwiftFunctionSignature.swift | 5 ++ .../JNI/JNIIsolatedParameterTests.swift | 73 +++++++++++++++++++ 5 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 Tests/JExtractSwiftTests/JNI/JNIIsolatedParameterTests.swift diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 4e94de345..b661af0a4 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -225,11 +225,22 @@ extension JNISwift2JavaGenerator { ) } + // Handle isolated methods + if decl.functionSignature.isIsolated { + self.convertToAsync( + translatedFunctionSignature: &translatedFunctionSignature, + nativeFunctionSignature: &nativeFunctionSignature, + originalFunctionSignature: decl.functionSignature, + mode: config.effectiveAsyncFuncMode, + ) + } + return TranslatedFunctionDecl( name: javaName, isStatic: decl.isStatic || !decl.hasParent || decl.isInitializer, isThrowing: decl.isThrowing, isAsync: decl.isAsync, + isIsolated: decl.isIsolated, nativeFunctionName: "$\(javaName)", parentName: parentName, functionTypes: funcTypes, @@ -1695,6 +1706,8 @@ extension JNISwift2JavaGenerator { var isAsync: Bool + var isIsolated: Bool + /// The name of the native function var nativeFunctionName: String @@ -1717,7 +1730,7 @@ extension JNISwift2JavaGenerator { func throwsClause() -> String { guard !translatedFunctionSignature.exceptions.isEmpty else { - return isThrowing && !isAsync ? " throws Exception" : "" + return isThrowing && !(isAsync || isIsolated) ? " 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 07ed35e11..3f553a902 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 { + if decl.isThrowing, !(decl.isAsync || decl.isIsolated) { printer.print("do {") printer.indent() printer.print(innerBody(in: &printer)) diff --git a/Sources/SwiftExtract/ExtractedDecls.swift b/Sources/SwiftExtract/ExtractedDecls.swift index 5c94dd2d6..12371beb3 100644 --- a/Sources/SwiftExtract/ExtractedDecls.swift +++ b/Sources/SwiftExtract/ExtractedDecls.swift @@ -369,6 +369,10 @@ public final class ExtractedFunc: ExtractedSwiftDecl, CustomStringConvertible { self.functionSignature.isAsync } + public var isIsolated: Bool { + self.functionSignature.isIsolated + } + public init( module: String, swiftDecl: any DeclSyntaxProtocol, diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift index 559cf9985..6d9445fa3 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift @@ -43,6 +43,11 @@ public struct SwiftFunctionSignature: Equatable { parameters.contains(where: \.isVariadic) } + /// Whether any parameter is marked `isolated`. + public var isIsolated: Bool { + parameters.contains(where: \.isIsolated) + } + public init( selfParameter: SwiftSelfParameter? = nil, parameters: [SwiftParameter], diff --git a/Tests/JExtractSwiftTests/JNI/JNIIsolatedParameterTests.swift b/Tests/JExtractSwiftTests/JNI/JNIIsolatedParameterTests.swift new file mode 100644 index 000000000..714d23462 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIIsolatedParameterTests.swift @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftJavaConfigurationShared +import Testing + +@Suite +struct JNIIsolatedParameterTests { + + static let source = """ + public actor MyActor { + public init() {} + } + public class Service { + public init() {} + public func run(on actor: isolated MyActor) throws -> Int { 0 } + } + """ + + @Test("Import: isolated throws -> Int (Java) is converted to a future") + func isolatedParameter_java() throws { + try assertOutput( + input: Self.source, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public java.util.concurrent.CompletableFuture run(MyActor actor) { + """, + """ + private static native void $run(long actor, long selfPointer, java.util.concurrent.CompletableFuture result_future); + """, + ] + ) + } + + @Test("Import: isolated throws -> Int (Swift) is awaited inside a Task") + func isolatedParameter_swift() throws { + try assertOutput( + input: Self.source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + task = Task.immediate { + ... + do { + let swiftResult$ = try await selfPointer$.pointee.run(on: actor$.pointee) + """ + ], + notExpectedChunks: [ + """ + do { + let swiftResult$ = try selfPointer$.pointee.run(on: actor$.pointee) + """ + ] + ) + } +} From c2d0bf9d96081859a7b7c9286f31bc6438358504 Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Fri, 14 Aug 2026 07:49:02 +0300 Subject: [PATCH 3/5] skip `isolated` 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 20319b5c2..9c6d3728d 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -132,6 +132,10 @@ struct CdeclLowering { } } + if signature.isIsolated { + throw LoweringError.isolatedParameterNotSupported() + } + // Lower the result. let loweredResult = try lowerResult(signature.result.type) @@ -1158,4 +1162,5 @@ enum LoweringError: Error { case inoutNotSupported(SwiftType, file: String = #file, line: Int = #line) 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) } From d2462b2fda75b371f746aa59d7f440239af99c2e Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Fri, 14 Aug 2026 10:22:49 +0300 Subject: [PATCH 4/5] Add runtime tests for using functions with `isolated` parameters --- .../Sources/MySwiftLibrary/Isolated.swift | 36 +++++++++++ .../java/com/example/swift/IsolatedTest.java | 59 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Isolated.swift create mode 100644 Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/IsolatedTest.java diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Isolated.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Isolated.swift new file mode 100644 index 000000000..da045e56e --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Isolated.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftJava + +public actor Counter { + var value: Int64 = 0 + + public init() {} +} + +public func increment(_ counter: isolated Counter, by amount: Int64) -> Int64 { + counter.value += amount + return counter.value +} + +public func reset(_ counter: isolated Counter) -> Int64 { + let value = counter.value + counter.value = 0 + return value +} + +public func incrementThrows(_ counter: isolated Counter) throws -> Int64 { + throw MySwiftError.swiftError +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/IsolatedTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/IsolatedTest.java new file mode 100644 index 000000000..9c6055685 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/IsolatedTest.java @@ -0,0 +1,59 @@ +//===----------------------------------------------------------------------===// +// +// 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.ExecutionException; +import java.util.concurrent.Future; + +import static org.junit.jupiter.api.Assertions.*; + +public class IsolatedTest { + @Test + void increment() throws Exception { + try (var arena = SwiftArena.ofConfined()) { + Counter counter = Counter.init(arena); + + Future afterFirstIncrement = MySwiftLibrary.increment(counter, 3); + assertEquals(3, afterFirstIncrement.get()); + + Future afterSecondIncrement = MySwiftLibrary.increment(counter, 4); + assertEquals(7, afterSecondIncrement.get()); + + Future reset = MySwiftLibrary.reset(counter); + assertEquals(7, reset.get()); + + Future resetAgain = MySwiftLibrary.reset(counter); + assertEquals(0, resetAgain.get()); + } + } + + @Test + void incrementThrows() throws Exception { + try (var arena = SwiftArena.ofConfined()) { + Counter counter = Counter.init(arena); + Future future = MySwiftLibrary.incrementThrows(counter); + + ExecutionException ex = assertThrows(ExecutionException.class, future::get); + + Throwable cause = ex.getCause(); + assertNotNull(cause); + assertEquals(Exception.class, cause.getClass()); + assertEquals("swiftError", cause.getMessage()); + } + } +} From 29c2e7200ecae165be93607248245887922a59e2 Mon Sep 17 00:00:00 2001 From: AbdAlRahman Gad Date: Fri, 14 Aug 2026 10:41:38 +0300 Subject: [PATCH 5/5] Combine handling of async and isolated methods in function signature conversion --- .../JNISwift2JavaGenerator+JavaTranslation.swift | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index b661af0a4..ed5aa581d 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -215,18 +215,8 @@ extension JNISwift2JavaGenerator { } } - // Handle async methods - if decl.functionSignature.isAsync { - self.convertToAsync( - translatedFunctionSignature: &translatedFunctionSignature, - nativeFunctionSignature: &nativeFunctionSignature, - originalFunctionSignature: decl.functionSignature, - mode: config.effectiveAsyncFuncMode, - ) - } - - // Handle isolated methods - if decl.functionSignature.isIsolated { + // Handle async methods and isolated methods + if decl.functionSignature.isAsync || decl.functionSignature.isIsolated { self.convertToAsync( translatedFunctionSignature: &translatedFunctionSignature, nativeFunctionSignature: &nativeFunctionSignature,