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 @@ -87,3 +87,20 @@ public enum GenericEnum<T> {
public func makeIntGenericEnum() -> GenericEnum<Int> {
if Bool.random() { return .foo } else { return .bar }
}

extension MyID where T: BinaryInteger {
// Conditional extension functions are not exported
public func computeSomeValue() -> Int {
Int(rawValue)
}
}

extension MyID where T == Int128 {
// Conditional extension functions are not exported
public func decomposed() -> (high: Int64, low: Int64) {
let value = self.rawValue
let high = Int64(truncatingIfNeeded: value >> 64)
let low = Int64(bitPattern: UInt64(truncatingIfNeeded: value))
return (high: high, low: low)
}
}
81 changes: 54 additions & 27 deletions Sources/JExtractSwiftLib/Swift2JavaVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ final class Swift2JavaVisitor {
var log: Logger { translator.log }

/// Constrained extensions deferred until specializations are applied
private var deferredConstrainedExtensions: [(ImportedNominalType, ExtensionDeclSyntax, String)] = []
private struct DeferredConstrainedExtension {
var node: ExtensionDeclSyntax
var sourceFilePath: String
var constraints: [(String, String)]
}
private var deferredConstrainedExtensions: [DeferredConstrainedExtension] = []

func visit(inputFile: SwiftJavaInputFile) {
let node = inputFile.syntax
Expand Down Expand Up @@ -119,16 +124,28 @@ final class Swift2JavaVisitor {
return
}

// If the extension has where-clause constraints, defer it until specializations are applied
let whereConstraints = parseWhereConstraints(node.genericWhereClause)
if !whereConstraints.isEmpty {
switch parseWhereConstraints(node.genericWhereClause) {
case .none:
break
case .unsupported:
log.debug(
"Skip importing constrained extension '\(node.extendedType.trimmedDescription)'; unsupported where-clause requirements: \(node.genericWhereClause?.trimmedDescription ?? "")"
)
return
case .sameType(let whereConstraints):
let matchingSpecializations = findMatchingSpecializations(
extendedType: importedNominalType,
whereConstraints: whereConstraints,
)
if matchingSpecializations.isEmpty {
// Specializations may not exist yet — defer for later
deferredConstrainedExtensions.append((importedNominalType, node, sourceFilePath))
deferredConstrainedExtensions.append(
.init(
node: node,
sourceFilePath: sourceFilePath,
constraints: whereConstraints
)
)
return
}

Expand Down Expand Up @@ -572,19 +589,21 @@ final class Swift2JavaVisitor {
}

// Process constrained extensions that were deferred
for (baseType, node, sourceFilePath) in deferredConstrainedExtensions {
let whereConstraints = parseWhereConstraints(node.genericWhereClause)
for deferred in deferredConstrainedExtensions {
guard let baseType = translator.importedNominalType(deferred.node.extendedType) else {
continue
}
let matchingSpecializations = findMatchingSpecializations(
extendedType: baseType,
whereConstraints: whereConstraints,
whereConstraints: deferred.constraints,
)
guard !matchingSpecializations.isEmpty else {
log.debug("Skipping deferred constrained extension of \(node.extendedType.trimmedDescription) — no matching specialization")
log.debug("Skipping deferred constrained extension of \(deferred.node.extendedType.trimmedDescription) — no matching specialization")
continue
}
for specialized in matchingSpecializations {
for memberItem in node.memberBlock.members {
self.visit(decl: memberItem.decl, in: specialized, sourceFilePath: sourceFilePath)
for memberItem in deferred.node.memberBlock.members {
self.visit(decl: memberItem.decl, in: specialized, sourceFilePath: deferred.sourceFilePath)
}
}
}
Expand All @@ -594,24 +613,32 @@ final class Swift2JavaVisitor {
// ==== -----------------------------------------------------------------------
// MARK: Constrained extension merging

/// Parse where clause constraints into a dictionary mapping param names to concrete types
private func parseWhereConstraints(_ whereClause: GenericWhereClauseSyntax?) -> [String: String] {
guard let whereClause else { return [:] }
var constraints: [String: String] = [:]
private enum ParsedWhereConstraints {
case none
case sameType([(String, String)])
case unsupported
}

private func parseWhereConstraints(_ whereClause: GenericWhereClauseSyntax?) -> ParsedWhereConstraints {
guard let whereClause else { return .none }
var constraints: [(String, String)] = []
for requirement in whereClause.requirements {
if case .sameTypeRequirement(let sameType) = requirement.requirement {
switch requirement.requirement {
case .sameTypeRequirement(let sameType):
let lhs = sameType.leftType.trimmedDescription
let rhs = sameType.rightType.trimmedDescription
constraints[lhs] = rhs
constraints.append((lhs, rhs))
case .conformanceRequirement, .layoutRequirement:
return .unsupported
}
}
return constraints
return .sameType(constraints)
}

/// Find specializations whose type args match the given where-clause constraints
private func findMatchingSpecializations(
extendedType: ImportedNominalType,
whereConstraints: [String: String],
whereConstraints: [(String, String)],
) -> [ImportedNominalType] {
guard let specializations = translator.specializations[extendedType] else {
return []
Expand All @@ -623,18 +650,18 @@ final class Swift2JavaVisitor {

/// Check if where clause constraints match a specialization's generic arguments
private func constraintsMatchSpecialization(
_ constraints: [String: String],
_ constraints: [(String, String)],
specialized: ImportedNominalType,
) -> Bool {
for (paramName, concreteType) in constraints {
if let expectedType = specialized.genericArguments[paramName] {
if expectedType != concreteType {
return false
}
for (lhs, rhs) in constraints {
if specialized.genericArguments[lhs] == rhs {
return true
}
if specialized.genericArguments[rhs] == lhs {
return true
}
// If the param isn't in the mapping, we allow it (might be a secondary constraint)
}
return true
return false
}
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.

Hmmm this seems suspicious, one sec

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.

Yeah this is wrong because we must match all same type requirements, I'll push a fix.

}

Expand Down
29 changes: 29 additions & 0 deletions Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -252,4 +252,33 @@ struct JNIGenericTypeTests {
]
)
}

@Test("Constrained extensions are ignored")
func constrainedExtensionsAreIgnored() throws {
let input =
#"""
public struct MyID<T> {}

extension MyID where T: BinaryInteger {
public func computeSomeValue() -> Int
}
extension MyID where T == Int128 {
public func decomposed() -> (high: Int64, low: Int64)
}
"""#

try assertOutput(
input: input,
.jni,
.java,
detectChunkByInitialLines: 1,
expectedChunks: [
"public final class MyID<T> implements JNISwiftInstance {"
],
notExpectedChunks: [
"computeSomeValue",
"decomposed",
],
)
}
}
23 changes: 19 additions & 4 deletions Tests/JExtractSwiftTests/SpecializationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,22 @@ struct SpecializationTests {
public var name: String
}

public struct Bait {
public var name: String
}

extension Box where Element == Fish {
public func observeTheFish() {}
}
extension Box where Fish == Element {
public func swappedObserveTheFish() {}
}
extension Box where Element == Bait {
public func observeTheBait() {}
}
extension Box where Bait == Element {
public func swappedObserveTheBait() {}
}

public typealias FishBox = Box<Fish>
public typealias ToolBox = Box<Tool>
Expand Down Expand Up @@ -141,13 +154,15 @@ struct SpecializationTests {
"public static FishBox wrapMemoryAddressUnsafe(long selfPointer, SwiftArena swiftArena)",
// Base method from Box<Element>
"public long count()",
// Method body must call FishBox's own native method, not Box's
"FishBox.$count(",
// Constrained extension method (Element == Fish)
"public void observeTheFish()",
// Constrained method body must also call FishBox's native method
"FishBox.$observeTheFish(",
// Constrained extension method (Fish == Element)
"public void swappedObserveTheFish()",
],
notExpectedChunks: [
"public void observeTheBait()",
"public void swappedObserveTheBait()",
]
)
}

Expand Down
Loading