Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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<Long> afterFirstIncrement = MySwiftLibrary.increment(counter, 3);
assertEquals(3, afterFirstIncrement.get());

Future<Long> afterSecondIncrement = MySwiftLibrary.increment(counter, 4);
assertEquals(7, afterSecondIncrement.get());

Future<Long> reset = MySwiftLibrary.reset(counter);
assertEquals(7, reset.get());

Future<Long> resetAgain = MySwiftLibrary.reset(counter);
assertEquals(0, resetAgain.get());
}
}

@Test
void incrementThrows() throws Exception {
try (var arena = SwiftArena.ofConfined()) {
Counter counter = Counter.init(arena);
Future<Long> 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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ struct CdeclLowering {
}
}

if signature.isIsolated {
throw LoweringError.isolatedParameterNotSupported()
}

// Lower the result.
let loweredResult = try lowerResult(signature.result.type)

Expand Down Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,8 @@ extension JNISwift2JavaGenerator {
}
}

// Handle async methods
if decl.functionSignature.isAsync {
// Handle async methods and isolated methods
if decl.functionSignature.isAsync || decl.functionSignature.isIsolated {
self.convertToAsync(
translatedFunctionSignature: &translatedFunctionSignature,
nativeFunctionSignature: &nativeFunctionSignature,
Expand All @@ -230,6 +230,7 @@ extension JNISwift2JavaGenerator {
isStatic: decl.isStatic || !decl.hasParent || decl.isInitializer,
isThrowing: decl.isThrowing,
isAsync: decl.isAsync,
isIsolated: decl.isIsolated,
nativeFunctionName: "$\(javaName)",
parentName: parentName,
functionTypes: funcTypes,
Expand Down Expand Up @@ -1695,6 +1696,8 @@ extension JNISwift2JavaGenerator {

var isAsync: Bool

var isIsolated: Bool

/// The name of the native function
var nativeFunctionName: String

Expand All @@ -1717,7 +1720,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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
4 changes: 4 additions & 0 deletions Sources/SwiftExtract/ExtractedDecls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
11 changes: 10 additions & 1 deletion Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -106,6 +110,7 @@ extension SwiftParameter {
self.isVariadic = false
self.hasDefaultValue = node.defaultValue != nil
self.defaultValueExpression = node.defaultValue?.value.trimmedDescription
self.isIsolated = false
}
}

Expand All @@ -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 {
Expand All @@ -128,6 +134,8 @@ extension SwiftParameter {
convention = .consuming
case .keyword(.inout):
convention = .inout
case .keyword(.isolated):
isIsolated = true
default:
sawUnknownSpecifier = true
break
Expand All @@ -140,6 +148,7 @@ extension SwiftParameter {
}
}
self.convention = convention
self.isIsolated = isIsolated

// Determine the type.
self.type = try SwiftType(type, lookupContext: lookupContext)
Expand Down
73 changes: 73 additions & 0 deletions Tests/JExtractSwiftTests/JNI/JNIIsolatedParameterTests.swift
Original file line number Diff line number Diff line change
@@ -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<java.lang.Long> run(MyActor actor) {
""",
"""
private static native void $run(long actor, long selfPointer, java.util.concurrent.CompletableFuture<java.lang.Long> 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)
"""
]
)
}
}
26 changes: 26 additions & 0 deletions Tests/SwiftExtractTests/AnalysisResultTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading