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,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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup that’s right! Sorry I didn’t send a complete snippet earlier

public distributed func hi() -> String {
"hi"
}
}

public func makeDistributedHi() -> DistributedHi {
DistributedHi(actorSystem: LocalTestingDistributedActorSystem())
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> greeting = greeter.hi();
assertEquals("hi", greeting.get());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Long> afterFirst = counter.incrementIsolated(3);
assertEquals(3, afterFirst.get());

Future<Long> 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<Long> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)

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

var isIsolated: Bool

var isImplicitlyAsync: Bool

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

Expand All @@ -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(
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 || decl.isIsolated) {
if decl.isThrowing, !(decl.isAsync || decl.isIsolated || decl.isImplicitlyAsync) {
printer.print("do {")
printer.indent()
printer.print(innerBody(in: &printer))
Expand Down
6 changes: 5 additions & 1 deletion Sources/SwiftExtract/ExtractedDecls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
24 changes: 24 additions & 0 deletions Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 4 additions & 0 deletions Sources/SwiftExtract/SwiftTypes/SwiftType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading