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
Expand Up @@ -27,3 +27,15 @@ public actor IdentifiedActor: ActorIdentifier {
self.unwitnessed = identifier + 1
}
}

public protocol SendableActorIdentifier: AnyObject, Sendable {
var identifier: Int64 { get }
}

public actor SendableIdentifiedActor: SendableActorIdentifier {
public let identifier: Int64

public init(identifier: Int64) {
self.identifier = identifier
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@
import static org.junit.jupiter.api.Assertions.*;

public class ActorPropertiesTest {
@Test
void standardProtocolConstraintsKeepWitnessesSynchronous() {
try (var arena = SwiftArena.ofConfined()) {
SendableIdentifiedActor actor = SendableIdentifiedActor.init(42, arena);
long identifier = actor.getIdentifier();
assertEquals(42, identifier);
}
}

@Test
void nonisolatedPropertiesStaySynchronous() {
try (var arena = SwiftArena.ofConfined()) {
Expand Down
10 changes: 9 additions & 1 deletion Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,15 @@ extension SwiftFunctionSignature {
else { return false }
if !visited.insert(declaration.identity).inserted { return true }
return (declaration.inheritanceTypes ?? []).allSatisfy { inherited in
guard let type = try? SwiftType(inherited.type, lookupContext: lookupContext) else { return false }
let type = try? SwiftType(inherited.type, lookupContext: lookupContext)
// These standard constraints do not introduce actor isolation and are
// absent from SwiftExtract's built-in Swift symbol table.
if ["AnyObject", "Swift.AnyObject", "Sendable", "Swift.Sendable"].contains(inherited.type.trimmedDescription),
type == nil || type?.asNominalTypeDeclaration?.moduleName == "Swift"
{
return true
}
guard let type else { return false }
return isNonisolatedProtocol(type, visited: &visited)
}
}
Expand Down
22 changes: 22 additions & 0 deletions Tests/JExtractSwiftTests/JNI/JNIActorPropertyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,26 @@ struct JNIActorPropertyTests {
]
)
}

@Test(arguments: ["AnyObject", "Swift.AnyObject", "Sendable", "Swift.Sendable", "AnyObject, Swift.Sendable"])
func standardConstraintsPreserveSynchronousBindings(_ inheritance: String) throws {
let source = """
public protocol Identified: \(inheritance) { var identifier: Int64 { get } }
public actor Record: Identified { public let identifier: Int64 }
"""
try assertOutput(
input: source,
.jni,
.java,
expectedChunks: ["public long getIdentifier() {"],
notExpectedChunks: ["public java.util.concurrent.CompletableFuture<java.lang.Long> getIdentifier() {"]
)
try assertOutput(
input: source,
.jni,
.swift,
expectedChunks: ["return (selfPointer$.pointee as any Identified).identifier.getJNILocalRefValue(in: environment)"],
notExpectedChunks: ["await selfPointer$.pointee.identifier"]
)
}
}
41 changes: 41 additions & 0 deletions Tests/SwiftExtractTests/ActorPropertyIsolationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,47 @@ struct ActorPropertyIsolationTests {
}
}

@Test(arguments: ["AnyObject", "Swift.AnyObject", "Sendable", "Swift.Sendable", "AnyObject, Swift.Sendable"])
func standardConstraintsPreserveSynchronousWitness(_ inheritance: String) throws {
let source = """
public protocol Identified: \(inheritance) { var identifier: Int64 { get } }
public actor Record: Identified { public let identifier: Int64 }
"""
let result = try analyze(sources: [("/fake/Source.swift", source)], moduleName: "Test")
let record = try #require(result.extractedTypes.values.first { $0.swiftNominal.name == "Record" })
let accessor = try #require(record.variables.first { $0.name == "identifier" })
#expect(accessor.functionSignature.nonisolatedWitnessProtocol?.asNominalTypeDeclaration?.name == "Identified")
#expect(!accessor.functionSignature.isImplicitlyAsync)
}

@Test
func inheritedStandardConstraintsPreserveSynchronousWitness() throws {
let source = """
public protocol Identified: AnyObject, Sendable { var identifier: Int64 { get } }
public protocol InheritedIdentifier: Identified {}
public actor Record: InheritedIdentifier { public let identifier: Int64 }
"""
let result = try analyze(sources: [("/fake/Source.swift", source)], moduleName: "Test")
let record = try #require(result.extractedTypes.values.first { $0.swiftNominal.name == "Record" })
let accessor = try #require(record.variables.first { $0.name == "identifier" })
#expect(accessor.functionSignature.nonisolatedWitnessProtocol?.asNominalTypeDeclaration?.name == "InheritedIdentifier")
#expect(!accessor.functionSignature.isImplicitlyAsync)
}

@Test
func localSendableDoesNotBypassIsolation() throws {
let source = """
@MainActor public protocol Sendable {}
public protocol Identified: Sendable { var identifier: Int64 { get } }
public actor Record: Identified { public let identifier: Int64 }
"""
let result = try analyze(sources: [("/fake/Source.swift", source)], moduleName: "Test")
let record = try #require(result.extractedTypes.values.first { $0.swiftNominal.name == "Record" })
let accessor = try #require(record.variables.first { $0.name == "identifier" })
#expect(accessor.functionSignature.nonisolatedWitnessProtocol == nil)
#expect(accessor.functionSignature.isImplicitlyAsync)
}

private func record() throws -> ExtractedNominalType {
let result = try analyze(sources: [("/fake/Source.swift", Self.source)], moduleName: "Test")
return try #require(result.extractedTypes.values.first { $0.swiftNominal.name == "Record" })
Expand Down
Loading