diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BoxSpecialization.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BoxSpecialization.swift new file mode 100644 index 00000000..31259a68 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BoxSpecialization.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-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 +// +//===----------------------------------------------------------------------===// + +public struct Box { + public var count: Int64 + + public init(count: Int64) { + self.count = count + } +} + +public struct Fish { + public var name: String + + public init(name: String) { + self.name = name + } +} + +extension Box where Element == Fish { + public func describeFish() -> String { + "A box of \(count) fish" + } +} + +public typealias FishBox = Box diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/BoxSpecializationTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/BoxSpecializationTest.java new file mode 100644 index 00000000..63b9e2b0 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/BoxSpecializationTest.java @@ -0,0 +1,59 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-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 java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.*; + +public class BoxSpecializationTest { + @Test + void fishBoxHasExpectedMethods() throws Exception { + // Verify FishBox class exists and has the expected methods + Class fishBoxClass = FishBox.class; + assertNotNull(fishBoxClass); + + // Base type property getter + Method getCount = fishBoxClass.getMethod("getCount"); + assertNotNull(getCount); + assertEquals(long.class, getCount.getReturnType()); + + // Base type property setter + Method setCount = fishBoxClass.getMethod("setCount", long.class); + assertNotNull(setCount); + + // Constrained extension method (only on FishBox, not on Box) + Method describeFish = fishBoxClass.getMethod("describeFish"); + assertNotNull(describeFish); + assertEquals(String.class, describeFish.getReturnType()); + } + + @Test + void fishBoxDoesNotHaveGenericTypeParameter() { + // FishBox is a concrete specialization — no generic type parameters + assertEquals(0, FishBox.class.getTypeParameters().length, + "FishBox should have no generic type parameters"); + } + + @Test + void boxHasGenericTypeParameter() { + // Box retains its generic parameter + assertEquals(1, Box.class.getTypeParameters().length, + "Box should have one generic type parameter"); + assertEquals("Element", Box.class.getTypeParameters()[0].getName()); + } +} diff --git a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift index 1b24d86c..c0808c37 100644 --- a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift @@ -31,7 +31,7 @@ extension ImplicitlyUnwrappedOptionalTypeSyntax { wrappedType: wrappedType, self.unexpectedBetweenWrappedTypeAndExclamationMark, self.unexpectedAfterExclamationMark, - trailingTrivia: self.trailingTrivia + trailingTrivia: self.trailingTrivia, ) } } @@ -128,7 +128,9 @@ extension WithModifiersSyntax { } extension AttributeListSyntax.Element { - /// Whether this node has `SwiftJava` attributes. + /// Whether this node has `SwiftJava` wrapping attributes (types that wrap Java classes). + /// These are skipped during jextract because they represent Java->Swift wrappers. + /// Note: `@JavaExport` is NOT included here — it forces export of Swift types to Java. var isJava: Bool { guard case let .attribute(attr) = self else { // FIXME: Handle #if. @@ -143,6 +145,14 @@ extension AttributeListSyntax.Element { return false } } + + /// Whether this is a `@JavaExport` attribute (used on typealiases for specialization, + /// or on struct/class/enum to force-include them even when excluded by filters) + var isJavaExport: Bool { + guard case let .attribute(attr) = self else { return false } + guard let attrName = attr.attributeName.as(IdentifierTypeSyntax.self)?.name.text else { return false } + return attrName == "JavaExport" + } } extension DeclSyntaxProtocol { @@ -260,7 +270,7 @@ extension DeclSyntaxProtocol { .with(\.accessorBlock, nil) .with(\.initializer, nil) } - ) + ), ) .triviaSanitizedDescription case .enumCaseDecl(let node): diff --git a/Sources/JExtractSwiftLib/ImportedDecls.swift b/Sources/JExtractSwiftLib/ImportedDecls.swift index 38f8c43a..e736f9a9 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls.swift +++ b/Sources/JExtractSwiftLib/ImportedDecls.swift @@ -29,38 +29,207 @@ package enum SwiftAPIKind: Equatable { /// Describes a Swift nominal type (e.g., a class, struct, enum) that has been /// imported and is being translated into Java. +/// +/// When `base` is non-nil, this is a specialization of a generic type +/// (e.g. `FishBox` specializing `Box` with `Element` = `Fish`). +/// The specialization delegates its member collections to the base type +/// so that extensions discovered later are visible through all specializations. package final class ImportedNominalType: ImportedDecl { let swiftNominal: SwiftNominalTypeDeclaration + /// If this type is a specialization (FishTank), then this points at the Tank base type of the specialization. + /// His allows simplified + package let specializationBaseType: ImportedNominalType? + // The short path from module root to the file in which this nominal was originally declared. // E.g. for `Sources/Example/My/Types.swift` it would be `My/Types.swift`. package var sourceFilePath: String { self.swiftNominal.sourceFilePath } - package var initializers: [ImportedFunc] = [] - package var methods: [ImportedFunc] = [] - package var variables: [ImportedFunc] = [] - package var cases: [ImportedEnumCase] = [] - var inheritedTypes: [SwiftType] - package var parent: SwiftNominalTypeDeclaration? + // Backing storage for member collections + private var _initializers: [ImportedFunc] = [] + private var _methods: [ImportedFunc] = [] + private var _variables: [ImportedFunc] = [] + private var _cases: [ImportedEnumCase] = [] + private var _inheritedTypes: [SwiftType] + private var _parent: SwiftNominalTypeDeclaration? + + // Additional members from constrained extensions that only apply to this specialization + package var constrainedInitializers: [ImportedFunc] = [] + package var constrainedMethods: [ImportedFunc] = [] + package var constrainedVariables: [ImportedFunc] = [] + + package var initializers: [ImportedFunc] { + get { + if let specializationBaseType { specializationBaseType.initializers + constrainedInitializers } else { _initializers } + } + set { + if let specializationBaseType { + let baseSet = Set(specializationBaseType.initializers.map { ObjectIdentifier($0) }) + constrainedInitializers = newValue.filter { !baseSet.contains(ObjectIdentifier($0)) } + } else { + _initializers = newValue + } + } + } + package var methods: [ImportedFunc] { + get { + if let specializationBaseType { specializationBaseType.methods + constrainedMethods } else { _methods } + } + set { + if let specializationBaseType { + let baseSet = Set(specializationBaseType.methods.map { ObjectIdentifier($0) }) + constrainedMethods = newValue.filter { !baseSet.contains(ObjectIdentifier($0)) } + } else { + _methods = newValue + } + } + } + package var variables: [ImportedFunc] { + get { + if let specializationBaseType { specializationBaseType.variables + constrainedVariables } else { _variables } + } + set { + if let specializationBaseType { + let baseSet = Set(specializationBaseType.variables.map { ObjectIdentifier($0) }) + constrainedVariables = newValue.filter { !baseSet.contains(ObjectIdentifier($0)) } + } else { + _variables = newValue + } + } + } + package var cases: [ImportedEnumCase] { + get { + if let specializationBaseType { specializationBaseType.cases } else { _cases } + } + set { + if let specializationBaseType { specializationBaseType.cases = newValue } else { _cases = newValue } + } + } + var inheritedTypes: [SwiftType] { + get { + if let specializationBaseType { specializationBaseType.inheritedTypes } else { _inheritedTypes } + } + set { + if let specializationBaseType { specializationBaseType.inheritedTypes = newValue } else { _inheritedTypes = newValue } + } + } + package var parent: SwiftNominalTypeDeclaration? { + get { + if let specializationBaseType { specializationBaseType.parent } else { _parent } + } + set { + if let specializationBaseType { specializationBaseType.parent = newValue } else { _parent = newValue } + } + } + + /// The Swift base type name, e.g. "Box" — always the unparameterized name + package var baseTypeName: String { swiftNominal.qualifiedName } + + /// The specialized/Java-facing name, e.g. "FishBox" — nil for base types + package private(set) var specializedTypeName: String? + + /// Whether this type is a specialization of a generic type + package var isSpecialization: Bool { specializationBaseType != nil } + + /// Generic parameter names (e.g. ["Element"] for Box). Empty for non-generic types + package var genericParameterNames: [String] { + swiftNominal.genericParameters.map(\.name) + } + + /// Maps generic parameter -> concrete type argument. Empty for unspecialized types + /// e.g. {"Element": "Fish"} for FishBox + package var genericArguments: [String: String] = [:] + + /// True when all generic parameters have corresponding arguments + package var isFullySpecialized: Bool { + !genericParameterNames.isEmpty && genericParameterNames.allSatisfy { genericArguments.keys.contains($0) } + } init(swiftNominal: SwiftNominalTypeDeclaration, lookupContext: SwiftTypeLookupContext) throws { self.swiftNominal = swiftNominal - self.inheritedTypes = + self.specializationBaseType = nil + self._inheritedTypes = swiftNominal.inheritanceTypes?.compactMap { try? SwiftType($0.type, lookupContext: lookupContext) } ?? [] - self.parent = swiftNominal.parent + self._parent = swiftNominal.parent + } + + /// Init for creating a specialization + private init(base: ImportedNominalType, specializedTypeName: String, genericArguments: [String: String]) { + self.swiftNominal = base.swiftNominal + self.specializationBaseType = base + self.specializedTypeName = specializedTypeName + self.genericArguments = genericArguments + self._inheritedTypes = [] } var swiftType: SwiftType { .nominal(.init(nominalTypeDecl: swiftNominal)) } + /// The effective Java-facing name — "FishBox" for specialized, "Box" for base + var effectiveJavaName: String { + specializedTypeName ?? swiftNominal.qualifiedName + } + + /// The simple Java class name (no qualification) for file naming purposes + var effectiveJavaSimpleName: String { + specializedTypeName ?? swiftNominal.name + } + + /// The Swift type for thunk generation — "Box" for specialized, "Box" for base + /// Computed from baseTypeName + genericArguments + var effectiveSwiftTypeName: String { + guard !genericArguments.isEmpty else { return baseTypeName } + let orderedArgs = genericParameterNames.compactMap { genericArguments[$0] } + guard !orderedArgs.isEmpty else { return baseTypeName } + return "\(baseTypeName)<\(orderedArgs.joined(separator: ", "))>" + } + var qualifiedName: String { self.swiftNominal.qualifiedName } + + /// The Java generic clause, e.g. "" for generic base types, "" for specialized or non-generic + var javaGenericClause: String { + if isSpecialization { + "" + } else if genericParameterNames.isEmpty { + "" + } else { + "<\(genericParameterNames.joined(separator: ", "))>" + } + } + + /// Create a specialized version of this generic type + package func specialize( + as specializedName: String, + with substitutions: [String: String], + ) throws -> ImportedNominalType { + guard !genericParameterNames.isEmpty else { + throw SpecializationError( + message: "Unable to specialize non-generic type '\(baseTypeName)' as '\(specializedName)'" + ) + } + let missingParams = genericParameterNames.filter { substitutions[$0] == nil } + guard missingParams.isEmpty else { + throw SpecializationError( + message: "Missing type arguments for: \(missingParams) when specializing \(baseTypeName) as \(specializedName)" + ) + } + return ImportedNominalType( + base: self, + specializedTypeName: specializedName, + genericArguments: substitutions, + ) + } +} + +struct SpecializationError: Error { + let message: String } public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible { @@ -82,7 +251,7 @@ public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible { parameters: [SwiftEnumCaseParameter], swiftDecl: any DeclSyntaxProtocol, enumType: SwiftNominalType, - caseFunction: ImportedFunc + caseFunction: ImportedFunc, ) { self.name = name self.parameters = parameters @@ -191,7 +360,7 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { swiftDecl: any DeclSyntaxProtocol, name: String, apiKind: SwiftAPIKind, - functionSignature: SwiftFunctionSignature + functionSignature: SwiftFunctionSignature, ) { self.module = module self.name = name diff --git a/Sources/JExtractSwiftLib/JNI/JNICaching.swift b/Sources/JExtractSwiftLib/JNI/JNICaching.swift index b0521946..446c6fc3 100644 --- a/Sources/JExtractSwiftLib/JNI/JNICaching.swift +++ b/Sources/JExtractSwiftLib/JNI/JNICaching.swift @@ -14,7 +14,7 @@ enum JNICaching { static func cacheName(for type: ImportedNominalType) -> String { - cacheName(for: type.swiftNominal.qualifiedName) + cacheName(for: type.effectiveJavaName) } static func cacheName(for type: SwiftNominalType) -> String { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index b21c7372..3a46d81d 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -49,17 +49,17 @@ extension JNISwift2JavaGenerator { // Each parent type goes into its own file // any nested types are printed inside the body as `static class` for (_, ty) in importedTypes.filter({ _, type in type.parent == nil }) { - let filename = "\(ty.swiftNominal.name).java" + let filename = "\(ty.effectiveJavaSimpleName).java" logger.debug("Printing contents: \(filename)") printImportedNominal(&printer, ty) if let outputFile = try printer.writeContents( outputDirectory: javaOutputDirectory, javaPackagePath: javaPackagePath, - filename: filename + filename: filename, ) { exportedFileNames.append(outputFile.path(percentEncoded: false)) - logger.info("[swift-java] Generated: \(ty.swiftNominal.name.bold).java (at \(outputFile))") + logger.info("[swift-java] Generated: \(ty.effectiveJavaSimpleName.bold).java (at \(outputFile))") } } @@ -70,7 +70,7 @@ extension JNISwift2JavaGenerator { if let outputFile = try printer.writeContents( outputDirectory: javaOutputDirectory, javaPackagePath: javaPackagePath, - filename: filename + filename: filename, ) { exportedFileNames.append(outputFile.path(percentEncoded: false)) logger.info("[swift-java] Generated: \(self.swiftModuleName).java (at \(outputFile))") @@ -82,7 +82,7 @@ extension JNISwift2JavaGenerator { try exportedFileNames.joined(separator: "\n").write( to: outputPath, atomically: true, - encoding: .utf8 + encoding: .utf8, ) logger.info("Generated file at \(outputPath)") } @@ -151,7 +151,7 @@ extension JNISwift2JavaGenerator { } let extendsString = extends.isEmpty ? "" : " extends \(extends.joined(separator: ", "))" - printer.printBraceBlock("public interface \(decl.swiftNominal.name)\(extendsString)") { printer in + printer.printBraceBlock("public interface \(decl.effectiveJavaSimpleName)\(extendsString)") { printer in for initializer in decl.initializers { self.logger.debug("Skipping static method '\(initializer.name)'") } @@ -177,6 +177,15 @@ extension JNISwift2JavaGenerator { } private func printConcreteType(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + let savedPrintingTypeName = self.currentPrintingTypeName + let savedPrintingType = self.currentPrintingType + self.currentPrintingTypeName = decl.effectiveJavaName + self.currentPrintingType = decl + defer { + self.currentPrintingTypeName = savedPrintingTypeName + self.currentPrintingType = savedPrintingType + } + printNominal(&printer, decl) { printer in printer.print( """ @@ -211,12 +220,14 @@ extension JNISwift2JavaGenerator { */ """ ) + // Specialized types are concrete — no selfTypePointer needed + let isEffectivelyGeneric = decl.swiftNominal.isGeneric && !decl.isSpecialization var swiftPointerParams = ["selfPointer"] - if decl.swiftNominal.isGeneric { + if isEffectivelyGeneric { swiftPointerParams.append("selfTypePointer") } let swiftPointerArg = swiftPointerParams.map { "long \($0)" }.joined(separator: ", ") - printer.printBraceBlock("private \(decl.swiftNominal.name)(\(swiftPointerArg), SwiftArena swiftArena)") { printer in + printer.printBraceBlock("private \(decl.effectiveJavaSimpleName)(\(swiftPointerArg), SwiftArena swiftArena)") { printer in for param in swiftPointerParams { printer.print( """ @@ -234,29 +245,25 @@ extension JNISwift2JavaGenerator { ) } printer.println() - let genericClause = - if decl.swiftNominal.isGeneric { - "<\(decl.swiftNominal.genericParameters.map(\.name).joined(separator: ", "))>" - } else { - "" - } + let genericClause = decl.javaGenericClause + let javaName = decl.effectiveJavaSimpleName printer.print( """ - /** - * Assume that the passed {@code long} represents a memory address of a {@link \(decl.swiftNominal.name)}. + /** + * Assume that the passed {@code long} represents a memory address of a {@link \(javaName)}. *

* Warnings: *

    - *
  • No checks are performed about the compatibility of the pointed at memory and the actual \(decl.swiftNominal.name) types.
  • + *
  • No checks are performed about the compatibility of the pointed at memory and the actual \(javaName) types.
  • *
  • This operation does not copy, or retain, the pointed at pointer, so its lifetime must be ensured manually to be valid when wrapping.
  • *
*/ - public static\(genericClause) \(decl.swiftNominal.name)\(genericClause) wrapMemoryAddressUnsafe(\(swiftPointerArg), SwiftArena swiftArena) { - return new \(decl.swiftNominal.name)\(genericClause)(\(swiftPointerParams.joined(separator: ", ")), swiftArena); + public static\(genericClause) \(javaName)\(genericClause) wrapMemoryAddressUnsafe(\(swiftPointerArg), SwiftArena swiftArena) { + return new \(javaName)\(genericClause)(\(swiftPointerParams.joined(separator: ", ")), swiftArena); } - public static\(genericClause) \(decl.swiftNominal.name)\(genericClause) wrapMemoryAddressUnsafe(\(swiftPointerArg)) { - return new \(decl.swiftNominal.name)\(genericClause)(\(swiftPointerParams.joined(separator: ", ")), SwiftMemoryManagement.DEFAULT_SWIFT_JAVA_AUTO_ARENA); + public static\(genericClause) \(javaName)\(genericClause) wrapMemoryAddressUnsafe(\(swiftPointerArg)) { + return new \(javaName)\(genericClause)(\(swiftPointerParams.joined(separator: ", ")), SwiftMemoryManagement.DEFAULT_SWIFT_JAVA_AUTO_ARENA); } """ ) @@ -280,7 +287,7 @@ extension JNISwift2JavaGenerator { """ ) - if decl.swiftNominal.isGeneric { + if isEffectivelyGeneric { printer.print("/** Pointer to the metatype of Self */") printer.print("private final long selfTypePointer;") } @@ -374,7 +381,7 @@ extension JNISwift2JavaGenerator { private func printNominal( _ printer: inout CodePrinter, _ decl: ImportedNominalType, - body: (inout CodePrinter) -> Void + body: (inout CodePrinter) -> Void, ) { if decl.swiftNominal.isSendable { printer.print("@ThreadSafe // Sendable") @@ -390,14 +397,10 @@ extension JNISwift2JavaGenerator { .filter { $0.kind == .protocol } .map(\.name) let implementsClause = implements.joined(separator: ", ") - let genericClause = - if decl.swiftNominal.isGeneric { - "<\(decl.swiftNominal.genericParameters.map(\.name).joined(separator: ", "))>" - } else { - "" - } + // Specialized types are concrete — no generic clause on the Java side + let genericClause = decl.javaGenericClause printer.printBraceBlock( - "\(modifiers.joined(separator: " ")) class \(decl.swiftNominal.name)\(genericClause) implements \(implementsClause)" + "\(modifiers.joined(separator: " ")) class \(decl.effectiveJavaSimpleName)\(genericClause) implements \(implementsClause)" ) { printer in body(&printer) } @@ -466,8 +469,9 @@ extension JNISwift2JavaGenerator { } private func printEnumStaticInitializers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { - if !decl.cases.isEmpty && decl.swiftNominal.isGeneric { - self.logger.debug("Skipping generic static initializers in '\(decl.swiftNominal.name)'") + let isEffectivelyGeneric = decl.swiftNominal.isGeneric && !decl.isSpecialization + if !decl.cases.isEmpty && isEffectivelyGeneric { + self.logger.debug("Skipping generic static initializers in '\(decl.effectiveJavaSimpleName)'") return } @@ -508,7 +512,7 @@ extension JNISwift2JavaGenerator { private func printFunctionDowncallMethods( _ printer: inout CodePrinter, _ decl: ImportedFunc, - skipMethodBody: Bool = false + skipMethodBody: Bool = false, ) { guard translatedDecl(for: decl) != nil else { // Failed to translate. Skip. @@ -527,7 +531,7 @@ extension JNISwift2JavaGenerator { /// * User-facing functional interfaces. private func printJavaBindingWrapperHelperClass( _ printer: inout CodePrinter, - _ decl: ImportedFunc + _ decl: ImportedFunc, ) { let translated = self.translatedDecl(for: decl)! if translated.functionTypes.isEmpty { @@ -548,7 +552,7 @@ extension JNISwift2JavaGenerator { /// Print "wrapper" functional interface representing a Swift closure type. func printJavaBindingWrapperFunctionTypeHelper( _ printer: inout CodePrinter, - _ functionType: TranslatedFunctionType + _ functionType: TranslatedFunctionType, ) { let apiParams = functionType.parameters.map({ $0.parameter.renderParameter() }) @@ -565,7 +569,7 @@ extension JNISwift2JavaGenerator { private func printJavaBindingWrapperMethod( _ printer: inout CodePrinter, _ decl: ImportedFunc, - skipMethodBody: Bool + skipMethodBody: Bool, ) { guard let translatedDecl = translatedDecl(for: decl) else { fatalError("Decl was not translated, \(decl)") @@ -577,7 +581,7 @@ extension JNISwift2JavaGenerator { _ printer: inout CodePrinter, _ translatedDecl: TranslatedFunctionDecl, importedFunc: ImportedFunc? = nil, - skipMethodBody: Bool + skipMethodBody: Bool, ) { var modifiers = ["public"] if translatedDecl.isStatic { @@ -622,7 +626,7 @@ extension JNISwift2JavaGenerator { TranslatedDocumentation.printDocumentation( importedFunc: importedFunc, translatedDecl: translatedDecl, - in: &printer + in: &printer, ) } var modifiers = modifiers @@ -654,7 +658,7 @@ extension JNISwift2JavaGenerator { TranslatedDocumentation.printDocumentation( importedFunc: importedFunc, translatedDecl: translatedDecl, - in: &printer + in: &printer, ) } let signature = @@ -691,7 +695,7 @@ extension JNISwift2JavaGenerator { private func printDowncall( _ printer: inout CodePrinter, - _ translatedDecl: TranslatedFunctionDecl + _ translatedDecl: TranslatedFunctionDecl, ) { let translatedFunctionSignature = translatedDecl.translatedFunctionSignature @@ -725,8 +729,9 @@ extension JNISwift2JavaGenerator { //=== Part 3: Downcall. // TODO: If we always generate a native method and a "public" method, we can actually choose our own thunk names // using the registry? + let effectiveParentName = self.currentPrintingTypeName ?? translatedDecl.parentName let downcall = - "\(translatedDecl.parentName).\(translatedDecl.nativeFunctionName)(\(arguments.joined(separator: ", ")))" + "\(effectiveParentName).\(translatedDecl.nativeFunctionName)(\(arguments.joined(separator: ", ")))" //=== Part 4: Convert the return value. if translatedFunctionSignature.resultType.javaType.isVoid { @@ -744,7 +749,8 @@ extension JNISwift2JavaGenerator { } private func printTypeMetadataAddressFunction(_ printer: inout CodePrinter, _ type: ImportedNominalType) { - if type.swiftNominal.isGeneric { + let isEffectivelyGeneric = type.swiftNominal.isGeneric && !type.isSpecialization + if isEffectivelyGeneric { printer.print("@Override") printer.printBraceBlock("public long $typeMetadataAddress()") { printer in printer.print("return this.selfTypePointer;") @@ -755,7 +761,7 @@ extension JNISwift2JavaGenerator { printer.printBraceBlock("public long $typeMetadataAddress()") { printer in // INFO: We are omitting `CallTraces.traceDowncall` here. // It internally calls `toString`, which in turn calls `$typeMetadataAddress`, creating an infinite loop. - printer.print("return \(type.swiftNominal.name).$typeMetadataAddressDowncall();") + printer.print("return \(type.effectiveJavaSimpleName).$typeMetadataAddressDowncall();") } } } @@ -763,15 +769,17 @@ extension JNISwift2JavaGenerator { /// Prints the destroy function for a `JNISwiftInstance` private func printDestroyFunction(_ printer: inout CodePrinter, _ type: ImportedNominalType) { let funcName = "$createDestroyFunction" + let isEffectivelyGeneric = type.swiftNominal.isGeneric && !type.isSpecialization + let typeName = type.effectiveJavaSimpleName printer.print("@Override") printer.printBraceBlock("public Runnable \(funcName)()") { printer in printer.print("long self$ = this.$memoryAddress();") printer.print("long selfType$ = this.$typeMetadataAddress();") - if type.swiftNominal.isGeneric { + if isEffectivelyGeneric { printer.print( """ if (CallTraces.TRACE_DOWNCALLS) { - CallTraces.traceDowncall("\(type.swiftNominal.name).\(funcName)", + CallTraces.traceDowncall("\(typeName).\(funcName)", "this", this, "self", self$, "selfType", selfType$); @@ -780,7 +788,7 @@ extension JNISwift2JavaGenerator { @Override public void run() { if (CallTraces.TRACE_DOWNCALLS) { - CallTraces.traceDowncall("\(type.swiftNominal.name).$destroy", "self", self$, "selfType", selfType$); + CallTraces.traceDowncall("\(typeName).$destroy", "self", self$, "selfType", selfType$); } SwiftObjects.destroy(self$, selfType$); } @@ -791,7 +799,7 @@ extension JNISwift2JavaGenerator { printer.print( """ if (CallTraces.TRACE_DOWNCALLS) { - CallTraces.traceDowncall("\(type.swiftNominal.name).\(funcName)", + CallTraces.traceDowncall("\(typeName).\(funcName)", "this", this, "self", self$); } @@ -799,7 +807,7 @@ extension JNISwift2JavaGenerator { @Override public void run() { if (CallTraces.TRACE_DOWNCALLS) { - CallTraces.traceDowncall("\(type.swiftNominal.name).$destroy", "self", self$); + CallTraces.traceDowncall("\(typeName).$destroy", "self", self$); } SwiftObjects.destroy(self$, selfType$); } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 72d27baa..dc28006b 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -26,7 +26,8 @@ extension JNISwift2JavaGenerator { knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), protocolWrappers: self.interfaceProtocolWrappers, logger: self.logger, - javaIdentifiers: self.currentJavaIdentifiers + javaIdentifiers: self.currentJavaIdentifiers, + importedTypes: self.analysis.importedTypes, ) } @@ -66,7 +67,8 @@ extension JNISwift2JavaGenerator { knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), protocolWrappers: self.interfaceProtocolWrappers, logger: self.logger, - javaIdentifiers: self.currentJavaIdentifiers + javaIdentifiers: self.currentJavaIdentifiers, + importedTypes: self.analysis.importedTypes, ) translated = try translation.translate(enumCase: decl) } catch { @@ -87,6 +89,7 @@ extension JNISwift2JavaGenerator { let protocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] let logger: Logger var javaIdentifiers: JavaIdentifierFactory + let importedTypes: [String: ImportedNominalType] func translate(enumCase: ImportedEnumCase) throws -> TranslatedEnumCase { let nativeTranslation = NativeJavaTranslation( @@ -95,7 +98,7 @@ extension JNISwift2JavaGenerator { javaClassLookupTable: self.javaClassLookupTable, knownTypes: self.knownTypes, protocolWrappers: self.protocolWrappers, - logger: self.logger + logger: self.logger, ) let methodName = "" // TODO: Used for closures, replace with better name? @@ -106,7 +109,7 @@ extension JNISwift2JavaGenerator { methodName: methodName, parentName: parentName, genericParameters: [], - genericRequirements: [] + genericRequirements: [], ) let conversions = try enumCase.parameters.enumerated().map { idx, parameter in @@ -115,7 +118,7 @@ extension JNISwift2JavaGenerator { var translatedResult = try self.translate(swiftResult: result, resultName: resultName) translatedResult.conversion = .replacingPlaceholder( translatedResult.conversion, - placeholder: "$nativeParameters.\(resultName)" + placeholder: "$nativeParameters.\(resultName)", ) let nativeResult = try nativeTranslation.translate(swiftResult: result, resultName: resultName) return (translated: translatedResult, native: nativeResult) @@ -132,9 +135,9 @@ extension JNISwift2JavaGenerator { arguments: [ .constructJavaClass( .commaSeparated(conversions.map(\.translated.conversion)), - .class(package: nil, name: caseName) + .class(package: nil, name: caseName), ) - ] + ], ) var exceptions: [JavaExceptionType] = [] @@ -159,17 +162,17 @@ extension JNISwift2JavaGenerator { [ .ifStatement( .constant("getDiscriminator() != Discriminator.\(caseName.uppercased())"), - thenExp: .constant("return Optional.empty();") + thenExp: .constant("return Optional.empty();"), ), .valueMemoryAddress(.placeholder), ] - ) + ), ), selfTypeParameter: !isGenericParent ? nil : .init( parameter: JavaParameter(name: "selfTypePointer", type: .long), - conversion: .typeMetadataAddress(.placeholder) + conversion: .typeMetadataAddress(.placeholder), ), parameters: [], resultType: TranslatedResult( @@ -177,16 +180,16 @@ extension JNISwift2JavaGenerator { outParameters: conversions.flatMap(\.translated.outParameters), conversion: enumCase.parameters.isEmpty ? constructRecordConversion - : .aggregate(variable: ("$nativeParameters", nativeParametersType), [constructRecordConversion]) + : .aggregate(variable: ("$nativeParameters", nativeParametersType), [constructRecordConversion]), ), - exceptions: exceptions + exceptions: exceptions, ), nativeFunctionSignature: NativeFunctionSignature( selfParameter: NativeParameter( parameters: [JavaParameter(name: "selfPointer", type: .long)], conversion: .extractSwiftValue(.placeholder, swiftType: .nominal(enumCase.enumType), allowNil: false), indirectConversion: nil, - conversionCheck: nil + conversionCheck: nil, ), selfTypeParameter: !isGenericParent ? nil @@ -194,15 +197,15 @@ extension JNISwift2JavaGenerator { parameters: [JavaParameter(name: "selfTypePointer", type: .long)], conversion: .extractMetatypeValue(.placeholder), indirectConversion: nil, - conversionCheck: nil + conversionCheck: nil, ), parameters: [], result: NativeResult( javaType: nativeParametersType, conversion: .placeholder, - outParameters: conversions.flatMap(\.native.outParameters) - ) - ) + outParameters: conversions.flatMap(\.native.outParameters), + ), + ), ) return TranslatedEnumCase( @@ -211,7 +214,7 @@ extension JNISwift2JavaGenerator { original: enumCase, translatedValues: translatedValues, parameterConversions: conversions, - getAsCaseFunction: getAsCaseFunction + getAsCaseFunction: getAsCaseFunction, ) } @@ -222,11 +225,18 @@ extension JNISwift2JavaGenerator { javaClassLookupTable: self.javaClassLookupTable, knownTypes: self.knownTypes, protocolWrappers: self.protocolWrappers, - logger: self.logger + logger: self.logger, ) // Types with no parent will be outputted inside a "module" class. - let parentName = decl.parentType?.asNominalType?.nominalTypeDecl.qualifiedName ?? swiftModuleName + // For specialized types, use the Java-facing name as the parent scope + let parentName: String + if let parentNominal = decl.parentType?.asNominalType?.nominalTypeDecl { + let importedParent = importedTypes.values.first { $0.swiftNominal === parentNominal } + parentName = importedParent?.effectiveJavaName ?? parentNominal.qualifiedName + } else { + parentName = swiftModuleName + } // Name. let javaName = javaIdentifiers.makeJavaMethodName(decl) @@ -235,14 +245,14 @@ extension JNISwift2JavaGenerator { var translatedFunctionSignature = try translate( functionSignature: decl.functionSignature, methodName: javaName, - parentName: parentName + parentName: parentName, ) // Java -> Java (native) var nativeFunctionSignature = try nativeTranslation.translate( functionSignature: decl.functionSignature, translatedFunctionSignature: translatedFunctionSignature, methodName: javaName, - parentName: parentName + parentName: parentName, ) // Closures. @@ -255,7 +265,7 @@ extension JNISwift2JavaGenerator { let translatedClosure = try translateFunctionType( name: parameterName, swiftType: funcTy, - parentName: parentName + parentName: parentName, ) funcTypes.append(translatedClosure) default: @@ -269,7 +279,7 @@ extension JNISwift2JavaGenerator { translatedFunctionSignature: &translatedFunctionSignature, nativeFunctionSignature: &nativeFunctionSignature, originalFunctionSignature: decl.functionSignature, - mode: config.effectiveAsyncFuncMode + mode: config.effectiveAsyncFuncMode, ) } @@ -282,7 +292,7 @@ extension JNISwift2JavaGenerator { parentName: parentName, functionTypes: funcTypes, translatedFunctionSignature: translatedFunctionSignature, - nativeFunctionSignature: nativeFunctionSignature + nativeFunctionSignature: nativeFunctionSignature, ) } @@ -290,7 +300,7 @@ extension JNISwift2JavaGenerator { func translateFunctionType( name: String, swiftType: SwiftFunctionType, - parentName: String + parentName: String, ) throws -> TranslatedFunctionType { var translatedParams: [TranslatedParameter] = [] @@ -304,7 +314,7 @@ extension JNISwift2JavaGenerator { parentName: parentName, genericParameters: [], genericRequirements: [], - parameterPosition: nil + parameterPosition: nil, ) ) } @@ -315,21 +325,21 @@ extension JNISwift2JavaGenerator { name: name, parameters: translatedParams, result: translatedResult, - swiftType: swiftType + swiftType: swiftType, ) } func translate( functionSignature: SwiftFunctionSignature, methodName: String, - parentName: String + parentName: String, ) throws -> TranslatedFunctionSignature { let parameters = try translateParameters( functionSignature.parameters.map { ($0.parameterName, $0.type) }, methodName: methodName, parentName: parentName, genericParameters: functionSignature.genericParameters, - genericRequirements: functionSignature.genericRequirements + genericRequirements: functionSignature.genericRequirements, ) // 'self' @@ -338,7 +348,7 @@ extension JNISwift2JavaGenerator { methodName: methodName, parentName: parentName, genericParameters: functionSignature.genericParameters, - genericRequirements: functionSignature.genericRequirements + genericRequirements: functionSignature.genericRequirements, ) let selfTypeParameter = try self.translateSelfTypeParameter( @@ -346,7 +356,7 @@ extension JNISwift2JavaGenerator { methodName: methodName, parentName: parentName, genericParameters: functionSignature.genericParameters, - genericRequirements: functionSignature.genericRequirements + genericRequirements: functionSignature.genericRequirements, ) var exceptions: [JavaExceptionType] = [] @@ -358,7 +368,7 @@ extension JNISwift2JavaGenerator { let resultType = try translate( swiftResult: functionSignature.result, genericParameters: functionSignature.genericParameters, - genericRequirements: functionSignature.genericRequirements + genericRequirements: functionSignature.genericRequirements, ) return TranslatedFunctionSignature( @@ -366,7 +376,7 @@ extension JNISwift2JavaGenerator { selfTypeParameter: selfTypeParameter, parameters: parameters, resultType: resultType, - exceptions: exceptions + exceptions: exceptions, ) } @@ -375,7 +385,7 @@ extension JNISwift2JavaGenerator { methodName: String, parentName: String, genericParameters: [SwiftGenericParameterDeclaration], - genericRequirements: [SwiftGenericRequirement] + genericRequirements: [SwiftGenericRequirement], ) throws -> [TranslatedParameter] { try parameters.enumerated().map { idx, param in let parameterName = param.name ?? "arg\(idx)" @@ -386,7 +396,7 @@ extension JNISwift2JavaGenerator { parentName: parentName, genericParameters: genericParameters, genericRequirements: genericRequirements, - parameterPosition: idx + parameterPosition: idx, ) } } @@ -396,7 +406,7 @@ extension JNISwift2JavaGenerator { methodName: String, parentName: String, genericParameters: [SwiftGenericParameterDeclaration], - genericRequirements: [SwiftGenericRequirement] + genericRequirements: [SwiftGenericRequirement], ) throws -> TranslatedParameter? { // 'self' if case .instance(_, let swiftType) = selfParameter { @@ -407,7 +417,7 @@ extension JNISwift2JavaGenerator { parentName: parentName, genericParameters: genericParameters, genericRequirements: genericRequirements, - parameterPosition: nil + parameterPosition: nil, ) } else { return nil @@ -419,7 +429,7 @@ extension JNISwift2JavaGenerator { methodName: String, parentName: String, genericParameters: [SwiftGenericParameterDeclaration], - genericRequirements: [SwiftGenericRequirement] + genericRequirements: [SwiftGenericRequirement], ) throws -> TranslatedParameter? { guard let selfParameter else { return nil @@ -434,7 +444,7 @@ extension JNISwift2JavaGenerator { parentName: parentName, genericParameters: genericParameters, genericRequirements: genericRequirements, - parameterPosition: nil + parameterPosition: nil, ) } else { return nil @@ -448,7 +458,7 @@ extension JNISwift2JavaGenerator { parentName: String, genericParameters: [SwiftGenericParameterDeclaration], genericRequirements: [SwiftGenericRequirement], - parameterPosition: Int? + parameterPosition: Int?, ) throws -> TranslatedParameter { // If the result type should cause any annotations on the method, include them here. @@ -465,7 +475,7 @@ extension JNISwift2JavaGenerator { wrappedType: wrapped, parameterName: parameterName, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) case .array(let elementType): @@ -473,7 +483,7 @@ extension JNISwift2JavaGenerator { elementType: elementType, parameterName: parameterName, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) case .dictionary(let keyType, let valueType): @@ -482,7 +492,7 @@ extension JNISwift2JavaGenerator { valueType: valueType, parameterName: parameterName, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) case .set(let elementType): @@ -490,7 +500,7 @@ extension JNISwift2JavaGenerator { elementType: elementType, parameterName: parameterName, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) case .foundationDate, .essentialsDate: @@ -502,7 +512,7 @@ extension JNISwift2JavaGenerator { case .foundationUUID, .essentialsUUID: return TranslatedParameter( parameter: JavaParameter(name: parameterName, type: .javaUtilUUID), - conversion: .method(.placeholder, function: "toString") + conversion: .method(.placeholder, function: "toString"), ) default: @@ -512,7 +522,7 @@ extension JNISwift2JavaGenerator { return TranslatedParameter( parameter: JavaParameter(name: parameterName, type: javaType, annotations: parameterAnnotations), - conversion: .placeholder + conversion: .placeholder, ) } } @@ -524,7 +534,7 @@ extension JNISwift2JavaGenerator { return TranslatedParameter( parameter: JavaParameter(name: parameterName, type: javaType, annotations: parameterAnnotations), - conversion: .placeholder + conversion: .placeholder, ) } @@ -535,9 +545,9 @@ extension JNISwift2JavaGenerator { try translateGenericTypeParameter( swiftType, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) - } ?? [] + } ?? [], ) // We assume this is a JExtract class. @@ -545,15 +555,15 @@ extension JNISwift2JavaGenerator { parameter: JavaParameter( name: parameterName, type: .concrete(javaType), - annotations: parameterAnnotations + annotations: parameterAnnotations, ), - conversion: .valueMemoryAddress(.placeholder) + conversion: .valueMemoryAddress(.placeholder), ) case .tuple([]): return TranslatedParameter( parameter: JavaParameter(name: parameterName, type: .void, annotations: parameterAnnotations), - conversion: .placeholder + conversion: .placeholder, ) case .function: @@ -561,9 +571,9 @@ extension JNISwift2JavaGenerator { parameter: JavaParameter( name: parameterName, type: .class(package: javaPackage, name: "\(parentName).\(methodName).\(parameterName)"), - annotations: parameterAnnotations + annotations: parameterAnnotations, ), - conversion: .placeholder + conversion: .placeholder, ) case .opaque(let proto), .existential(let proto): @@ -576,20 +586,20 @@ extension JNISwift2JavaGenerator { parameterName: parameterName, javaGenericName: "_T\(parameterPosition)", genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) case .genericParameter(let generic): if let concreteTy = swiftType.typeIn( genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) { return try translateProtocolParameter( protocolType: concreteTy, parameterName: parameterName, javaGenericName: generic.name, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) } @@ -598,7 +608,7 @@ extension JNISwift2JavaGenerator { case .metatype: return TranslatedParameter( parameter: JavaParameter(name: parameterName, type: .long), - conversion: .typeMetadataAddress(.placeholder) + conversion: .typeMetadataAddress(.placeholder), ) case .tuple(let elements) where !elements.isEmpty: @@ -609,7 +619,7 @@ extension JNISwift2JavaGenerator { parentName: parentName, genericParameters: genericParameters, genericRequirements: genericRequirements, - parameterPosition: parameterPosition + parameterPosition: parameterPosition, ) case .tuple, .composite: @@ -624,7 +634,7 @@ extension JNISwift2JavaGenerator { parentName: String, genericParameters: [SwiftGenericParameterDeclaration], genericRequirements: [SwiftGenericRequirement], - parameterPosition: Int? + parameterPosition: Int?, ) throws -> TranslatedParameter { var elementJavaTypes: [JavaType] = [] @@ -638,13 +648,13 @@ extension JNISwift2JavaGenerator { parentName: parentName, genericParameters: genericParameters, genericRequirements: genericRequirements, - parameterPosition: parameterPosition + parameterPosition: parameterPosition, ) // Extract the element from the tuple using .$N field access let extraction = JavaNativeConversionStep.replacingPlaceholder( elementTranslated.conversion, - placeholder: "\(parameterName).$\(idx)" + placeholder: "\(parameterName).$\(idx)", ) elementConversions.append(extraction) elementJavaTypes.append(elementTranslated.parameter.type.javaType) @@ -655,9 +665,9 @@ extension JNISwift2JavaGenerator { return TranslatedParameter( parameter: JavaParameter( name: parameterName, - type: javaType + type: javaType, ), - conversion: .commaSeparated(elementConversions) + conversion: .commaSeparated(elementConversions), ) } @@ -665,7 +675,7 @@ extension JNISwift2JavaGenerator { translatedFunctionSignature: inout TranslatedFunctionSignature, nativeFunctionSignature: inout NativeFunctionSignature, originalFunctionSignature: SwiftFunctionSignature, - mode: JExtractAsyncFuncMode + mode: JExtractAsyncFuncMode, ) { // Update translated function let nativeFutureType: JavaType @@ -690,7 +700,7 @@ extension JNISwift2JavaGenerator { let futureOutParameter = OutParameter( name: "future$", type: nativeFutureType, - allocation: .new + allocation: .new, ) let result = translatedFunctionSignature.resultType @@ -716,7 +726,7 @@ extension JNISwift2JavaGenerator { nativeFunctionSignature: nativeFunctionSignature, isThrowing: originalFunctionSignature.isThrowing, completeMethodID: completeMethodID, - completeExceptionallyMethodID: completeExceptionallyMethodID + completeExceptionallyMethodID: completeExceptionallyMethodID, ) nativeFunctionSignature.result.javaType = .void nativeFunctionSignature.result.outParameters.append(.init(name: "result_future", type: nativeFutureType)) @@ -727,7 +737,7 @@ extension JNISwift2JavaGenerator { parameterName: String, javaGenericName: String, genericParameters: [SwiftGenericParameterDeclaration], - genericRequirements: [SwiftGenericRequirement] + genericRequirements: [SwiftGenericRequirement], ) throws -> TranslatedParameter { switch protocolType { case .nominal: @@ -736,7 +746,7 @@ extension JNISwift2JavaGenerator { parameterName: parameterName, javaGenericName: javaGenericName, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) case .composite(let types): @@ -745,7 +755,7 @@ extension JNISwift2JavaGenerator { parameterName: parameterName, javaGenericName: javaGenericName, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) default: @@ -758,13 +768,13 @@ extension JNISwift2JavaGenerator { parameterName: String, javaGenericName: String, genericParameters: [SwiftGenericParameterDeclaration], - genericRequirements: [SwiftGenericRequirement] + genericRequirements: [SwiftGenericRequirement], ) throws -> TranslatedParameter { let javaProtocolTypes = try protocolTypes.map { try translateGenericTypeParameter( $0, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) } @@ -773,9 +783,9 @@ extension JNISwift2JavaGenerator { parameter: JavaParameter( name: parameterName, type: .generic(name: javaGenericName, extends: javaProtocolTypes), - annotations: [] + annotations: [], ), - conversion: .placeholder + conversion: .placeholder, ) } @@ -783,7 +793,7 @@ extension JNISwift2JavaGenerator { wrappedType swiftType: SwiftType, parameterName: String, genericParameters: [SwiftGenericParameterDeclaration], - genericRequirements: [SwiftGenericRequirement] + genericRequirements: [SwiftGenericRequirement], ) throws -> TranslatedParameter { let parameterAnnotations: [JavaAnnotation] = getTypeAnnotations(swiftType: swiftType, config: config) @@ -815,12 +825,12 @@ extension JNISwift2JavaGenerator { parameter: JavaParameter( name: parameterName, type: JavaType(className: translatedClass), - annotations: parameterAnnotations + annotations: parameterAnnotations, ), conversion: .commaSeparated([ .isOptionalPresent, .method(.placeholder, function: "orElse", arguments: [.constant(placeholderValue)]), - ]) + ]), ) } } @@ -834,13 +844,13 @@ extension JNISwift2JavaGenerator { parameter: JavaParameter( name: parameterName, type: .class(package: nil, name: "Optional<\(javaType)>"), - annotations: parameterAnnotations + annotations: parameterAnnotations, ), conversion: .method( .placeholder, function: "orElse", - arguments: [.constant("null")] - ) + arguments: [.constant("null")], + ), ) } @@ -848,19 +858,19 @@ extension JNISwift2JavaGenerator { let javaType = try translateGenericTypeParameter( swiftType, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) return TranslatedParameter( parameter: JavaParameter( name: parameterName, type: .class(package: nil, name: "Optional", typeParameters: [javaType]), - annotations: parameterAnnotations + annotations: parameterAnnotations, ), conversion: .method( .method(.placeholder, function: "map", arguments: [.constant("\(javaType)::$memoryAddress")]), function: "orElse", - arguments: [.constant("0L")] - ) + arguments: [.constant("0L")], + ), ) default: throw JavaTranslationError.unsupportedSwiftType(swiftType) @@ -871,7 +881,7 @@ extension JNISwift2JavaGenerator { swiftResult: SwiftResult, resultName: String = "result", genericParameters: [SwiftGenericParameterDeclaration] = [], - genericRequirements: [SwiftGenericRequirement] = [] + genericRequirements: [SwiftGenericRequirement] = [], ) throws -> TranslatedResult { let swiftType = swiftResult.type @@ -890,7 +900,7 @@ extension JNISwift2JavaGenerator { wrappedType: genericArgs[0], resultName: resultName, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) case .array: @@ -900,7 +910,7 @@ extension JNISwift2JavaGenerator { return try translateArrayResult( elementType: elementType, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) case .dictionary: @@ -911,7 +921,7 @@ extension JNISwift2JavaGenerator { keyType: genericArgs[0], valueType: genericArgs[1], genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) case .set: @@ -921,7 +931,7 @@ extension JNISwift2JavaGenerator { return try translateSetResult( elementType: genericArgs[0], genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) case .foundationDate, .essentialsDate: @@ -939,8 +949,8 @@ extension JNISwift2JavaGenerator { conversion: .method( .constant("java.util.UUID"), function: "fromString", - arguments: [.placeholder] - ) + arguments: [.placeholder], + ), ) default: @@ -952,7 +962,7 @@ extension JNISwift2JavaGenerator { javaType: javaType, annotations: resultAnnotations, outParameters: [], - conversion: .placeholder + conversion: .placeholder, ) } } @@ -968,9 +978,9 @@ extension JNISwift2JavaGenerator { try translateGenericTypeParameter( swiftType, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) - } ?? [] + } ?? [], ) // We assume this is a JExtract class. @@ -992,7 +1002,7 @@ extension JNISwift2JavaGenerator { javaType: javaType, annotations: resultAnnotations, outParameters: [], - conversion: .wrapMemoryAddressUnsafe(.placeholder, javaType) + conversion: .wrapMemoryAddressUnsafe(.placeholder, javaType), ) } @@ -1004,7 +1014,7 @@ extension JNISwift2JavaGenerator { elements: elements, resultName: resultName, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) case .metatype, .tuple, .function, .existential, .opaque, .genericParameter, .composite: @@ -1015,7 +1025,7 @@ extension JNISwift2JavaGenerator { private func translateGenericTypeParameter( _ swiftType: SwiftType, genericParameters: [SwiftGenericParameterDeclaration], - genericRequirements: [SwiftGenericRequirement] + genericRequirements: [SwiftGenericRequirement], ) throws -> JavaType { switch swiftType { case .nominal(let nominalType): @@ -1030,7 +1040,7 @@ extension JNISwift2JavaGenerator { let wrappedType = try translateGenericTypeParameter( genericArgs[0], genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) return .class(package: "java.util", name: "Optional", typeParameters: [wrappedType]) @@ -1041,7 +1051,7 @@ extension JNISwift2JavaGenerator { let elementJavaType = try translateGenericTypeParameter( elementType, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) return .array(elementJavaType) @@ -1052,12 +1062,12 @@ extension JNISwift2JavaGenerator { let keyJavaType = try translateGenericTypeParameter( genericArgs[0], genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) let valueJavaType = try translateGenericTypeParameter( genericArgs[1], genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) return .swiftDictionaryMap(keyJavaType, valueJavaType) @@ -1068,7 +1078,7 @@ extension JNISwift2JavaGenerator { let elementJavaType = try translateGenericTypeParameter( genericArgs[0], genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) return .swiftSet(elementJavaType) @@ -1102,25 +1112,25 @@ extension JNISwift2JavaGenerator { try translateGenericTypeParameter( swiftType, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) } ?? [] return .class( package: nil, name: nominalTypeName, - typeParameters: typeParameters + typeParameters: typeParameters, ) case .genericParameter(let generic): if let concreteTy = swiftType.typeIn( genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) { return try translateGenericTypeParameter( concreteTy, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) } return .class(package: nil, name: generic.name) @@ -1134,7 +1144,7 @@ extension JNISwift2JavaGenerator { elements: [SwiftTupleElement], resultName: String = "result", genericParameters: [SwiftGenericParameterDeclaration], - genericRequirements: [SwiftGenericRequirement] + genericRequirements: [SwiftGenericRequirement], ) throws -> TranslatedResult { let arity = elements.count var outParameters: [OutParameter] = [] @@ -1150,7 +1160,7 @@ extension JNISwift2JavaGenerator { swiftResult: .init(convention: .indirect, type: element.type), resultName: outParamName, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) elementOutParamNames.append(outParamName) @@ -1185,11 +1195,44 @@ extension JNISwift2JavaGenerator { ) } + /// Translate a single element type for tuple results on the Java side. + private func translateTupleElementResult( + type: SwiftType, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement], + ) throws -> (JavaType, JavaNativeConversionStep) { + switch type { + case .nominal(let nominalType): + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + // Primitives: just read from array + return (javaType, .placeholder) + } + + guard !nominalType.isSwiftJavaWrapper else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + + let javaType = try translateGenericTypeParameter( + type, + genericParameters: genericParameters, + genericRequirements: genericRequirements, + ) + // JExtract class: wrap memory address + return (.long, .constructSwiftValue(.placeholder, javaType)) + + default: + throw JavaTranslationError.unsupportedSwiftType(type) + } + } + func translateOptionalResult( wrappedType swiftType: SwiftType, resultName: String, genericParameters: [SwiftGenericParameterDeclaration], - genericRequirements: [SwiftGenericRequirement] + genericRequirements: [SwiftGenericRequirement], ) throws -> TranslatedResult { let discriminatorName = "\(resultName)$_discriminator$" @@ -1229,8 +1272,8 @@ extension JNISwift2JavaGenerator { resultName: resultName, valueType: javaType, valueSizeInBytes: nextIntergralTypeWithSpaceForByte.valueBytes, - optionalType: optionalClass - ) + optionalType: optionalClass, + ), ) } else { // Otherwise, we return the result as normal, but @@ -1246,8 +1289,8 @@ extension JNISwift2JavaGenerator { optionalClass: optionalClass, nativeResultJavaType: javaType, toValue: .placeholder, - resultName: resultName - ) + resultName: resultName, + ), ) } } @@ -1261,7 +1304,7 @@ extension JNISwift2JavaGenerator { let javaType = try translateGenericTypeParameter( swiftType, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) let wrappedValueResult = try translate( @@ -1304,7 +1347,7 @@ extension JNISwift2JavaGenerator { elementType: SwiftType, parameterName: String, genericParameters: [SwiftGenericParameterDeclaration], - genericRequirements: [SwiftGenericRequirement] + genericRequirements: [SwiftGenericRequirement], ) throws -> TranslatedParameter { let parameterAnnotations: [JavaAnnotation] = getTypeAnnotations(swiftType: elementType, config: config) @@ -1317,7 +1360,7 @@ extension JNISwift2JavaGenerator { return TranslatedParameter( parameter: JavaParameter(name: parameterName, type: .array(javaType), annotations: parameterAnnotations), - conversion: .requireNonNull(.placeholder, message: "\(parameterName) must not be null") + conversion: .requireNonNull(.placeholder, message: "\(parameterName) must not be null"), ) } @@ -1328,24 +1371,24 @@ extension JNISwift2JavaGenerator { let javaType = try translateGenericTypeParameter( elementType, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) // Assume JExtract imported class return TranslatedParameter( parameter: JavaParameter( name: parameterName, type: .array(javaType), - annotations: parameterAnnotations + annotations: parameterAnnotations, ), conversion: .method( .method( .arraysStream(.requireNonNull(.placeholder, message: "\(parameterName) must not be null")), function: "mapToLong", - arguments: [.constant("\(javaType)::$memoryAddress")] + arguments: [.constant("\(javaType)::$memoryAddress")], ), function: "toArray", - arguments: [] - ) + arguments: [], + ), ) default: @@ -1356,7 +1399,7 @@ extension JNISwift2JavaGenerator { func translateArrayResult( elementType: SwiftType, genericParameters: [SwiftGenericParameterDeclaration], - genericRequirements: [SwiftGenericRequirement] + genericRequirements: [SwiftGenericRequirement], ) throws -> TranslatedResult { let annotations: [JavaAnnotation] = getTypeAnnotations(swiftType: elementType, config: config) @@ -1371,7 +1414,7 @@ extension JNISwift2JavaGenerator { javaType: .array(javaType), annotations: annotations, outParameters: [], - conversion: .placeholder + conversion: .placeholder, ) } @@ -1382,7 +1425,7 @@ extension JNISwift2JavaGenerator { let javaType = try translateGenericTypeParameter( elementType, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) // We assume this is a JExtract class. return TranslatedResult( @@ -1396,9 +1439,9 @@ extension JNISwift2JavaGenerator { arguments: [ .lambda( args: ["pointer"], - body: .wrapMemoryAddressUnsafe(.constant("pointer"), javaType) + body: .wrapMemoryAddressUnsafe(.constant("pointer"), javaType), ) - ] + ], ), function: "toArray", arguments: [.constant("\(javaType.className!)[]::new")] @@ -1413,12 +1456,12 @@ extension JNISwift2JavaGenerator { func javaTypeForDictionaryComponent( _ swiftType: SwiftType, genericParameters: [SwiftGenericParameterDeclaration], - genericRequirements: [SwiftGenericRequirement] + genericRequirements: [SwiftGenericRequirement], ) throws -> JavaType { try translateGenericTypeParameter( swiftType, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) } @@ -1427,17 +1470,17 @@ extension JNISwift2JavaGenerator { valueType: SwiftType, parameterName: String, genericParameters: [SwiftGenericParameterDeclaration], - genericRequirements: [SwiftGenericRequirement] + genericRequirements: [SwiftGenericRequirement], ) throws -> TranslatedParameter { let keyJavaType = try javaTypeForDictionaryComponent( keyType, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) let valueJavaType = try javaTypeForDictionaryComponent( valueType, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) let dictType = JavaType.swiftDictionaryMap(keyJavaType, valueJavaType) @@ -1446,8 +1489,8 @@ extension JNISwift2JavaGenerator { conversion: .method( .requireNonNull(.placeholder, message: "\(parameterName) must not be null"), function: "$memoryAddress", - arguments: [] - ) + arguments: [], + ), ) } @@ -1455,24 +1498,24 @@ extension JNISwift2JavaGenerator { keyType: SwiftType, valueType: SwiftType, genericParameters: [SwiftGenericParameterDeclaration], - genericRequirements: [SwiftGenericRequirement] + genericRequirements: [SwiftGenericRequirement], ) throws -> TranslatedResult { let keyJavaType = try javaTypeForDictionaryComponent( keyType, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) let valueJavaType = try javaTypeForDictionaryComponent( valueType, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) let dictType = JavaType.swiftDictionaryMap(keyJavaType, valueJavaType) return TranslatedResult( javaType: dictType, outParameters: [], - conversion: .wrapMemoryAddressUnsafe(.placeholder, dictType) + conversion: .wrapMemoryAddressUnsafe(.placeholder, dictType), ) } @@ -1480,12 +1523,12 @@ extension JNISwift2JavaGenerator { elementType: SwiftType, parameterName: String, genericParameters: [SwiftGenericParameterDeclaration], - genericRequirements: [SwiftGenericRequirement] + genericRequirements: [SwiftGenericRequirement], ) throws -> TranslatedParameter { let elementJavaType = try javaTypeForDictionaryComponent( elementType, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) let setType = JavaType.swiftSet(elementJavaType) @@ -1494,27 +1537,27 @@ extension JNISwift2JavaGenerator { conversion: .method( .requireNonNull(.placeholder, message: "\(parameterName) must not be null"), function: "$memoryAddress", - arguments: [] - ) + arguments: [], + ), ) } func translateSetResult( elementType: SwiftType, genericParameters: [SwiftGenericParameterDeclaration], - genericRequirements: [SwiftGenericRequirement] + genericRequirements: [SwiftGenericRequirement], ) throws -> TranslatedResult { let elementJavaType = try javaTypeForDictionaryComponent( elementType, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) let setType = JavaType.swiftSet(elementJavaType) return TranslatedResult( javaType: setType, outParameters: [], - conversion: .wrapMemoryAddressUnsafe(.placeholder, setType) + conversion: .wrapMemoryAddressUnsafe(.placeholder, setType), ) } } @@ -1699,13 +1742,13 @@ extension JNISwift2JavaGenerator { resultName: String, valueType: JavaType, valueSizeInBytes: Int, - optionalType: String + optionalType: String, ) indirect case ternary( JavaNativeConversionStep, thenExp: JavaNativeConversionStep, - elseExp: JavaNativeConversionStep + elseExp: JavaNativeConversionStep, ) indirect case equals(JavaNativeConversionStep, JavaNativeConversionStep) @@ -1717,7 +1760,7 @@ extension JNISwift2JavaGenerator { optionalClass: String, nativeResultJavaType: JavaType, toValue valueConversion: JavaNativeConversionStep, - resultName: String + resultName: String, ) -> JavaNativeConversionStep { .aggregate( variable: nativeResultJavaType.isVoid ? nil : (name: "\(resultName)$", type: nativeResultJavaType), @@ -1725,12 +1768,12 @@ extension JNISwift2JavaGenerator { .ternary( .equals( .subscriptOf(discriminatorName, arguments: [.constant("0")]), - .constant("1") + .constant("1"), ), thenExp: .method(.constant(optionalClass), function: "of", arguments: [valueConversion]), - elseExp: .method(.constant(optionalClass), function: "empty") + elseExp: .method(.constant(optionalClass), function: "empty"), ) - ] + ], ) } @@ -1740,7 +1783,7 @@ extension JNISwift2JavaGenerator { indirect case ifStatement( JavaNativeConversionStep, thenExp: JavaNativeConversionStep, - elseExp: JavaNativeConversionStep? = nil + elseExp: JavaNativeConversionStep? = nil, ) /// Access a member of the value @@ -1833,7 +1876,7 @@ extension JNISwift2JavaGenerator { let resultName, let valueType, let valueSizeInBytes, - let optionalType + let optionalType, ): let combined = combined.render(&printer, placeholder) printer.print( @@ -2009,7 +2052,7 @@ extension JNISwift2JavaGenerator { static func unsupportedSwiftType( _ type: SwiftType, _fileID: String = #fileID, - _line: Int = #line + _line: Int = #line, ) -> JavaTranslationError { .unsupportedSwiftType(type, fileID: _fileID, line: _line) } @@ -2018,7 +2061,7 @@ extension JNISwift2JavaGenerator { static func unsupportedSwiftType( known type: SwiftKnownType, _fileID: String = #fileID, - _line: Int = #line + _line: Int = #line, ) -> JavaTranslationError { .unsupportedSwiftType(known: type, fileID: _fileID, line: _line) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 7ecddb9f..a3af6496 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -47,7 +47,7 @@ extension JNISwift2JavaGenerator { _ = try printer.writeContents( outputDirectory: self.swiftOutputDirectory, javaPackagePath: nil, - filename: expectedFileName + filename: expectedFileName, ) } } @@ -64,7 +64,7 @@ extension JNISwift2JavaGenerator { if let outputFile = try printer.writeContents( outputDirectory: self.swiftOutputDirectory, javaPackagePath: nil, - filename: moduleFilename + filename: moduleFilename, ) { logger.info("Generated: \(moduleFilenameBase.bold).swift (at \(outputFile.absoluteString))") self.expectedOutputSwiftFileNames.remove(moduleFilename) @@ -75,7 +75,7 @@ extension JNISwift2JavaGenerator { // because otherwise SwiftPM plugins will not pick up files apropriately -- we expect 1 output +SwiftJava.swift file for every input. for group: (key: String, value: [Dictionary.Element]) in Dictionary( grouping: self.analysis.importedTypes, - by: { $0.value.sourceFilePath } + by: { $0.value.sourceFilePath }, ) { logger.warning("Writing types in file group: \(group.key): \(group.value.map(\.key))") @@ -87,14 +87,14 @@ extension JNISwift2JavaGenerator { let filename = "\(inputFileName)".replacing(".swift", with: "+SwiftJava.swift") for ty in importedTypesForThisFile { - logger.info("Printing Swift thunks for type: \(ty.qualifiedName.bold)") - printer.printSeparator("Thunks for \(ty.qualifiedName)") + logger.info("Printing Swift thunks for type: \(ty.effectiveJavaName.bold)") + printer.printSeparator("Thunks for \(ty.effectiveJavaName)") do { try printNominalTypeThunks(&printer, ty) } catch { logger.warning( - "Failed to print to Swift thunks for type'\(ty.qualifiedName)' to '\(filename)', error: \(error)" + "Failed to print to Swift thunks for type'\(ty.effectiveJavaName)' to '\(filename)', error: \(error)" ) } @@ -105,7 +105,7 @@ extension JNISwift2JavaGenerator { if let outputFile = try printer.writeContents( outputDirectory: self.swiftOutputDirectory, javaPackagePath: nil, - filename: filename + filename: filename, ) { logger.info("Done writing Swift thunks to: \(outputFile.absoluteString)") self.expectedOutputSwiftFileNames.remove(filename) @@ -157,7 +157,7 @@ extension JNISwift2JavaGenerator { try contents.write( toFile: outputPath, atomically: true, - encoding: .utf8 + encoding: .utf8, ) logger.info("[swift-java] Generated linker export list (\(generatedCDeclSymbolNames.count) symbols): \(outputPath)") } @@ -176,7 +176,7 @@ extension JNISwift2JavaGenerator { /// Prints the extension needed to make allow upcalls from Swift to Java for protocols private func printSwiftInterfaceWrapper( _ printer: inout CodePrinter, - _ translatedWrapper: JavaInterfaceSwiftWrapper + _ translatedWrapper: JavaInterfaceSwiftWrapper, ) throws { printer.printBraceBlock("protocol \(translatedWrapper.wrapperName): \(translatedWrapper.swiftName)") { printer in printer.print( @@ -201,7 +201,7 @@ extension JNISwift2JavaGenerator { private func printInterfaceWrapperFunctionImpl( _ printer: inout CodePrinter, _ function: JavaInterfaceSwiftWrapper.Function, - inside wrapper: JavaInterfaceSwiftWrapper + inside wrapper: JavaInterfaceSwiftWrapper, ) throws { guard let protocolMethod = wrapper.importedType.methods.first(where: { @@ -232,7 +232,7 @@ extension JNISwift2JavaGenerator { printer.printBraceBlock("\(returnStmt)\(withLocalFrameTryKeyword) environment$.withLocalFrame(capacity: \(estimatedRefCount))") { printer in var upcallArguments = zip( function.originalFunctionSignature.parameters, - function.parameterConversions + function.parameterConversions, ).map { param, conversion in // Wrap-java does not extract parameter names, so no labels conversion.render(&printer, param.parameterName!) @@ -257,7 +257,7 @@ extension JNISwift2JavaGenerator { private func printerInterfaceWrapperVariable( _ printer: inout CodePrinter, _ variable: JavaInterfaceSwiftWrapper.Variable, - inside wrapper: JavaInterfaceSwiftWrapper + inside wrapper: JavaInterfaceSwiftWrapper, ) { // FIXME: Add support for variables. This won't get printed yet // so we no need to worry about fatalErrors. @@ -303,7 +303,19 @@ extension JNISwift2JavaGenerator { } private func printConcreteTypeThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) { - if type.swiftNominal.isGeneric { + let savedPrintingTypeName = self.currentPrintingTypeName + let savedPrintingType = self.currentPrintingType + self.currentPrintingTypeName = type.effectiveJavaName + self.currentPrintingType = type + defer { + self.currentPrintingTypeName = savedPrintingTypeName + self.currentPrintingType = savedPrintingType + } + + // Specialized types are treated as concrete even if the underlying Swift type is generic + let isEffectivelyGeneric = type.swiftNominal.isGeneric && !type.isSpecialization + + if isEffectivelyGeneric { printOpenerProtocol(&printer, type) printer.println() } @@ -317,7 +329,7 @@ extension JNISwift2JavaGenerator { printEnumRawDiscriminator(&printer, type) printer.println() - if !type.swiftNominal.isGeneric { + if !isEffectivelyGeneric { for enumCase in type.cases { printEnumCase(&printer, enumCase) printer.println() @@ -353,7 +365,7 @@ extension JNISwift2JavaGenerator { return } - printer.printBraceBlock("extension \(type.swiftNominal.qualifiedName): _RawDiscriminatorRepresentable") { printer in + printer.printBraceBlock("extension \(type.effectiveSwiftTypeName): _RawDiscriminatorRepresentable") { printer in printer.printBraceBlock("public var _rawDiscriminator: Int32") { printer in printer.printBraceBlock("switch self") { printer in for (idx, enumCase) in type.cases.enumerated() { @@ -383,7 +395,7 @@ extension JNISwift2JavaGenerator { let nativeParametersClassName = "\(enumCase.enumName)$\(enumCase.name)$_NativeParameters" let methodSignature = MethodSignature( resultType: .void, - parameterTypes: enumCase.parameterConversions.map(\.native.javaType) + parameterTypes: enumCase.parameterConversions.map(\.native.javaType), ) return renderJNICacheInit(className: nativeParametersClassName, methods: [("", methodSignature)]) @@ -400,15 +412,15 @@ extension JNISwift2JavaGenerator { private func printEnumGetAsCaseThunk( _ printer: inout CodePrinter, - _ enumCase: TranslatedEnumCase + _ enumCase: TranslatedEnumCase, ) { printCDecl( &printer, - enumCase.getAsCaseFunction + enumCase.getAsCaseFunction, ) { printer in let selfPointer = enumCase.getAsCaseFunction.nativeFunctionSignature.selfParameter!.conversion.render( &printer, - "selfPointer" + "selfPointer", ) let caseNames = enumCase.original.parameters.enumerated().map { idx, parameter in parameter.name ?? "_\(idx)" @@ -416,7 +428,7 @@ extension JNISwift2JavaGenerator { let caseNamesWithLet = caseNames.map { "let \($0)" } let methodSignature = MethodSignature( resultType: .void, - parameterTypes: enumCase.parameterConversions.map(\.native.javaType) + parameterTypes: enumCase.parameterConversions.map(\.native.javaType), ) printer.print( """ @@ -445,7 +457,7 @@ extension JNISwift2JavaGenerator { private func printSwiftFunctionThunk( _ printer: inout CodePrinter, - _ decl: ImportedFunc + _ decl: ImportedFunc, ) { guard let translatedDecl = translatedDecl(for: decl) else { // Failed to translate. Skip. @@ -456,10 +468,15 @@ extension JNISwift2JavaGenerator { printCDecl( &printer, - translatedDecl + translatedDecl, ) { printer in if let parent = decl.parentType?.asNominalType, parent.nominalTypeDecl.isGeneric { - self.printFunctionOpenerCall(&printer, decl) + if self.currentPrintingType?.isSpecialization == true { + // Specializations use direct calls with concrete type, not protocol opening + self.printFunctionDowncall(&printer, decl) + } else { + self.printFunctionOpenerCall(&printer, decl) + } } else { self.printFunctionDowncall(&printer, decl) } @@ -468,12 +485,12 @@ extension JNISwift2JavaGenerator { private func printSwiftFunctionHelperClasses( _ printer: inout CodePrinter, - _ decl: ImportedFunc + _ decl: ImportedFunc, ) { let protocolParameters = decl.functionSignature.parameters.compactMap { parameter in if let concreteType = parameter.type.typeIn( genericParameters: decl.functionSignature.genericParameters, - genericRequirements: decl.functionSignature.genericRequirements + genericRequirements: decl.functionSignature.genericRequirements, ) { return (parameter, concreteType) } @@ -524,7 +541,7 @@ extension JNISwift2JavaGenerator { let swiftClassName = JNISwift2JavaGenerator.protocolParameterWrapperClassName( methodName: decl.name, parameterName: parameterName, - parentName: decl.parentType?.asNominalType?.nominalTypeDecl.qualifiedName ?? swiftModuleName + parentName: decl.parentType?.asNominalType?.nominalTypeDecl.qualifiedName ?? swiftModuleName, ) let implementingProtocols = protocolWrappers.map(\.wrapperName).joined(separator: ", ") @@ -559,7 +576,7 @@ extension JNISwift2JavaGenerator { private func printFunctionDowncall( _ printer: inout CodePrinter, - _ decl: ImportedFunc + _ decl: ImportedFunc, ) { guard let translatedDecl = self.translatedDecl(for: decl) else { fatalError("Cannot print function downcall for a function that can't be translated: \(decl)") @@ -587,7 +604,7 @@ extension JNISwift2JavaGenerator { int32OverflowChecks.append( parameter.conversionCheck!.render( &printer, - JNISwift2JavaGenerator.indirectVariableName(for: javaParameterName) + JNISwift2JavaGenerator.indirectVariableName(for: javaParameterName), ) ) case nil: @@ -616,10 +633,19 @@ extension JNISwift2JavaGenerator { let callee: String = switch decl.functionSignature.selfParameter { case .instance: - nativeSignature.selfParameter!.conversion.render( - &printer, - "selfPointer" - ) + if let specializedType = self.currentPrintingType, specializedType.isSpecialization { + // For specializations, use the concrete Swift type for pointer casting + // (the cached conversion uses the raw generic type name which won't compile) + self.renderSpecializedSelfPointer( + &printer, + concreteSwiftType: specializedType.effectiveSwiftTypeName, + ) + } else { + nativeSignature.selfParameter!.conversion.render( + &printer, + "selfPointer", + ) + } case .staticMethod(let selfType), .initializer(let selfType): "\(selfType)" case .none: @@ -632,7 +658,7 @@ extension JNISwift2JavaGenerator { case .function, .initializer: let downcallArguments = zip( decl.functionSignature.parameters, - arguments + arguments, ).map { originalParam, argument in let label = originalParam.argumentLabel.map { "\($0): " } ?? "" return "\(label)\(argument)" @@ -643,7 +669,7 @@ extension JNISwift2JavaGenerator { case .enumCase: let downcallArguments = zip( decl.functionSignature.parameters, - arguments + arguments, ).map { originalParam, argument in let label = originalParam.argumentLabel.map { "\($0): " } ?? "" return "\(label)\(argument)" @@ -721,7 +747,7 @@ extension JNISwift2JavaGenerator { private func printCDecl( _ printer: inout CodePrinter, _ translatedDecl: TranslatedFunctionDecl, - _ body: (inout CodePrinter) -> Void + _ body: (inout CodePrinter) -> Void, ) { let nativeSignature = translatedDecl.nativeFunctionSignature var parameters = nativeSignature.parameters.flatMap(\.parameters) @@ -737,9 +763,9 @@ extension JNISwift2JavaGenerator { printCDecl( &printer, javaMethodName: translatedDecl.nativeFunctionName, - parentName: translatedDecl.parentName, + parentName: self.currentPrintingTypeName ?? translatedDecl.parentName, parameters: parameters, - resultType: nativeSignature.result.javaType + resultType: nativeSignature.result.javaType, ) { printer in body(&printer) } @@ -751,7 +777,7 @@ extension JNISwift2JavaGenerator { parentName: String, parameters: [JavaParameter], resultType: JavaType, - _ body: (inout CodePrinter) -> Void + _ body: (inout CodePrinter) -> Void, ) { let jniSignature = parameters.reduce(into: "") { signature, parameter in signature += parameter.type.jniTypeSignature @@ -808,20 +834,22 @@ extension JNISwift2JavaGenerator { } private func printTypeMetadataAddressThunk(_ printer: inout CodePrinter, _ type: ImportedNominalType) { - if type.swiftNominal.isGeneric { + // Specialized types are treated as concrete + let isEffectivelyGeneric = type.swiftNominal.isGeneric && !type.isSpecialization + if isEffectivelyGeneric { return } printCDecl( &printer, javaMethodName: "$typeMetadataAddressDowncall", - parentName: type.swiftNominal.qualifiedName, + parentName: type.effectiveJavaName, parameters: [], - resultType: .long + resultType: .long, ) { printer in printer.print( """ - let metadataPointer = unsafeBitCast(\(type.swiftNominal.qualifiedName).self, to: UnsafeRawPointer.self) + let metadataPointer = unsafeBitCast(\(type.effectiveSwiftTypeName).self, to: UnsafeRawPointer.self) return Int64(Int(bitPattern: metadataPointer)).getJNIValue(in: environment) """ ) @@ -851,11 +879,11 @@ extension JNISwift2JavaGenerator { printCDecl( &printer, javaMethodName: "$toByteArray", - parentName: type.swiftNominal.qualifiedName, + parentName: type.effectiveJavaName, parameters: [ selfPointerParam ], - resultType: .array(.byte) + resultType: .array(.byte), ) { printer in let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam) @@ -872,11 +900,11 @@ extension JNISwift2JavaGenerator { printCDecl( &printer, javaMethodName: "$toByteArrayIndirectCopy", - parentName: type.swiftNominal.qualifiedName, + parentName: type.effectiveJavaName, parameters: [ selfPointerParam ], - resultType: .array(.byte) + resultType: .array(.byte), ) { printer in let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam) @@ -988,13 +1016,34 @@ extension JNISwift2JavaGenerator { } } + /// Renders self pointer extraction for a specialized (concrete) type. + /// Used instead of the generic opener mechanism when we know the exact type at compile time. + /// + /// - Returns: name of the created "self" variable (e.g., "selfPointer$") + private func renderSpecializedSelfPointer( + _ printer: inout CodePrinter, + concreteSwiftType: String, + ) -> String { + printer.print( + """ + assert(selfPointer != 0, "selfPointer memory address was null") + let selfPointerBits$ = Int(Int64(fromJNI: selfPointer, in: environment)) + let selfPointer$ = UnsafeMutablePointer<\(concreteSwiftType)>(bitPattern: selfPointerBits$) + guard let selfPointer$ else { + fatalError("selfPointer memory address was null in call to \\(#function)!") + } + """ + ) + return "selfPointer$.pointee" + } + /// Print the necessary conversion logic to go from a `jlong` to a `UnsafeMutablePointer` /// /// - Returns: name of the created "self" variable private func printSelfJLongToUnsafeMutablePointer( _ printer: inout CodePrinter, swiftParentName: String, - _ selfPointerParam: JavaParameter + _ selfPointerParam: JavaParameter, ) -> String { let newSelfParamName = "selfPointer$" printer.print( @@ -1015,7 +1064,7 @@ extension JNISwift2JavaGenerator { static func protocolParameterWrapperClassName( methodName: String, parameterName: String, - parentName: String? + parentName: String?, ) -> String { let parent = if let parentName { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index 5a3a9407..07b72487 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -52,6 +52,15 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { /// Duplicate identifier tracking for the current batch of methods being generated. var currentJavaIdentifiers: JavaIdentifierFactory = JavaIdentifierFactory() + /// The Java-facing name of the type currently being printed. + /// Used to override cached parentName in translations (needed for specializations + /// where the same ImportedFunc is shared between base and specialized types) + var currentPrintingTypeName: String? + + /// The type currently being printed (Java class or Swift thunks). + /// Used to determine specialization context for correct code generation + var currentPrintingType: ImportedNominalType? + /// Because we need to write empty files for SwiftPM, keep track which files we didn't write yet, /// and write an empty file for those. /// @@ -65,7 +74,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { javaPackage: String, swiftOutputDirectory: String, javaOutputDirectory: String, - javaClassLookupTable: JavaClassLookupTable + javaClassLookupTable: JavaClassLookupTable, ) { self.config = config self.logger = Logger(label: "jni-generator", logLevel: translator.log.logLevel) diff --git a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift b/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift index badab2e9..a5d96a1a 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift @@ -52,6 +52,9 @@ public final class Swift2JavaTranslator { /// type representation. package var importedTypes: [String: ImportedNominalType] = [:] + /// Specializations of generic types that will get their concrete Java declarations, "as if" they were independent types + package var specializations: [ImportedNominalType: Set] = [:] + var lookupContext: SwiftTypeLookupContext! = nil var symbolTable: SwiftSymbolTable! { @@ -105,6 +108,9 @@ extension Swift2JavaTranslator { visitor.visit(inputFile: input) } + // Apply any specializations registered after their target types were visited + visitor.applyPendingSpecializations() + self.visitFoundationDeclsIfNeeded(with: visitor) } diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index 44e9c66f..57a9cd9c 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -29,6 +29,9 @@ final class Swift2JavaVisitor { var log: Logger { translator.log } + /// Constrained extensions deferred until specializations are applied + private var deferredConstrainedExtensions: [(ImportedNominalType, ExtensionDeclSyntax, String)] = [] + func visit(inputFile: SwiftJavaInputFile) { let node = inputFile.syntax for codeItem in node.statements { @@ -52,8 +55,8 @@ final class Swift2JavaVisitor { self.visit(nominalDecl: node, in: parent, sourceFilePath: sourceFilePath) case .extensionDecl(let node): self.visit(extensionDecl: node, in: parent, sourceFilePath: sourceFilePath) - case .typeAliasDecl: - break // TODO: Implement; https://github.com/swiftlang/swift-java/issues/338 + case .typeAliasDecl(let node): + self.visit(typeAliasDecl: node, in: parent, sourceFilePath: sourceFilePath) case .associatedTypeDecl: break // TODO: Implement associated types @@ -77,11 +80,15 @@ final class Swift2JavaVisitor { nominalDecl node: some DeclSyntaxProtocol & DeclGroupSyntax & NamedDeclSyntax & WithAttributesSyntax & WithModifiersSyntax, in parent: ImportedNominalType?, - sourceFilePath: String + sourceFilePath: String, ) { guard let importedNominalType = translator.importedNominalType(node, parent: parent) else { return } + + // Check if there's a specialization entry for this type + applySpecialization(to: importedNominalType) + for memberItem in node.memberBlock.members { self.visit(decl: memberItem.decl, in: importedNominalType, sourceFilePath: sourceFilePath) } @@ -90,7 +97,7 @@ final class Swift2JavaVisitor { func visit( enumDecl node: EnumDeclSyntax, in parent: ImportedNominalType?, - sourceFilePath: String + sourceFilePath: String, ) { self.visit(nominalDecl: node, in: parent, sourceFilePath: sourceFilePath) @@ -100,7 +107,7 @@ final class Swift2JavaVisitor { func visit( extensionDecl node: ExtensionDeclSyntax, in parent: ImportedNominalType?, - sourceFilePath: String + sourceFilePath: String, ) { guard parent == nil else { // 'extension' in a nominal type is invalid. Ignore @@ -110,7 +117,29 @@ final class Swift2JavaVisitor { return } - // Add any conforming protocols in the extension + // If the extension has where-clause constraints, defer it until specializations are applied + let whereConstraints = parseWhereConstraints(node.genericWhereClause) + if !whereConstraints.isEmpty { + let matchingSpecializations = findMatchingSpecializations( + extendedType: importedNominalType, + whereConstraints: whereConstraints, + ) + if matchingSpecializations.isEmpty { + // Specializations may not exist yet — defer for later + deferredConstrainedExtensions.append((importedNominalType, node, sourceFilePath)) + return + } + + // Visit members in each matching specialization, not the base type + for specialized in matchingSpecializations { + for memberItem in node.memberBlock.members { + self.visit(decl: memberItem.decl, in: specialized, sourceFilePath: sourceFilePath) + } + } + return + } + + // Unconstrained extension — add to the base type (visible through all specializations) importedNominalType.inheritedTypes += node.inheritanceClause?.inheritedTypes.compactMap { try? SwiftType($0.type, lookupContext: translator.lookupContext) @@ -124,7 +153,7 @@ final class Swift2JavaVisitor { func visit( functionDecl node: FunctionDeclSyntax, in typeContext: ImportedNominalType?, - sourceFilePath: String + sourceFilePath: String, ) { guard node.shouldExtract(config: config, log: log, in: typeContext) else { return @@ -145,7 +174,7 @@ final class Swift2JavaVisitor { signature = try SwiftFunctionSignature( node, enclosingType: typeContext?.swiftType, - lookupContext: translator.lookupContext + lookupContext: translator.lookupContext, ) } catch { self.log.debug("Failed to import: '\(node.qualifiedNameForDebug)'; \(error)") @@ -157,10 +186,10 @@ final class Swift2JavaVisitor { swiftDecl: node, name: node.name.text, apiKind: .function, - functionSignature: signature + functionSignature: signature, ) - if typeContext?.swiftNominal.isGeneric == true && imported.isStatic { + if typeContext?.swiftNominal.isGeneric == true && typeContext?.isSpecialization != true && imported.isStatic { log.debug("Skip importing static function in generic type: '\(node.qualifiedNameForDebug)'") return } @@ -175,7 +204,7 @@ final class Swift2JavaVisitor { func visit( enumCaseDecl node: EnumCaseDeclSyntax, - in typeContext: ImportedNominalType? + in typeContext: ImportedNominalType?, ) { guard let typeContext else { self.log.info("Enum case must be within a current type; \(node)") @@ -193,7 +222,7 @@ final class Swift2JavaVisitor { let signature = try SwiftFunctionSignature( caseElement, enclosingType: typeContext.swiftType, - lookupContext: translator.lookupContext + lookupContext: translator.lookupContext, ) let caseFunction = ImportedFunc( @@ -201,7 +230,7 @@ final class Swift2JavaVisitor { swiftDecl: node, name: caseElement.name.text, apiKind: .enumCase, - functionSignature: signature + functionSignature: signature, ) let importedCase = ImportedEnumCase( @@ -209,7 +238,7 @@ final class Swift2JavaVisitor { parameters: parameters ?? [], swiftDecl: node, enumType: SwiftNominalType(nominalTypeDecl: typeContext.swiftNominal), - caseFunction: caseFunction + caseFunction: caseFunction, ) typeContext.cases.append(importedCase) @@ -222,7 +251,7 @@ final class Swift2JavaVisitor { func visit( variableDecl node: VariableDeclSyntax, in typeContext: ImportedNominalType?, - sourceFilePath: String + sourceFilePath: String, ) { guard node.shouldExtract(config: config, log: log, in: typeContext) else { return @@ -243,7 +272,7 @@ final class Swift2JavaVisitor { from: DeclSyntax(node), in: typeContext, kind: .getter, - name: varName + name: varName, ) } if supportedAccessors.contains(.set) { @@ -251,7 +280,7 @@ final class Swift2JavaVisitor { from: DeclSyntax(node), in: typeContext, kind: .setter, - name: varName + name: varName, ) } } catch { @@ -271,7 +300,7 @@ final class Swift2JavaVisitor { return } - if typeContext.swiftNominal.isGeneric { + if typeContext.swiftNominal.isGeneric && !typeContext.isSpecialization { log.debug("Skip Importing generic type initializer \(node.kind) '\(node.qualifiedNameForDebug)'") return } @@ -283,7 +312,7 @@ final class Swift2JavaVisitor { signature = try SwiftFunctionSignature( node, enclosingType: typeContext.swiftType, - lookupContext: translator.lookupContext + lookupContext: translator.lookupContext, ) } catch { self.log.debug("Failed to import: \(node.qualifiedNameForDebug); \(error)") @@ -294,7 +323,7 @@ final class Swift2JavaVisitor { swiftDecl: node, name: "init", apiKind: .initializer, - functionSignature: signature + functionSignature: signature, ) typeContext.initializers.append(imported) @@ -321,7 +350,7 @@ final class Swift2JavaVisitor { from: DeclSyntax(node), in: typeContext, kind: .subscriptGetter, - name: name + name: name, ) } if accessors.contains(.set) { @@ -329,7 +358,7 @@ final class Swift2JavaVisitor { from: DeclSyntax(node), in: typeContext, kind: .subscriptSetter, - name: name + name: name, ) } } catch { @@ -341,7 +370,7 @@ final class Swift2JavaVisitor { from node: DeclSyntax, in typeContext: ImportedNominalType?, kind: SwiftAPIKind, - name: String + name: String, ) throws { let signature: SwiftFunctionSignature @@ -351,14 +380,14 @@ final class Swift2JavaVisitor { varNode, isSet: kind == .setter, enclosingType: typeContext?.swiftType, - lookupContext: translator.lookupContext + lookupContext: translator.lookupContext, ) case .subscriptDecl(let subscriptNode): signature = try SwiftFunctionSignature( subscriptNode, isSet: kind == .subscriptSetter, enclosingType: typeContext?.swiftType, - lookupContext: translator.lookupContext + lookupContext: translator.lookupContext, ) default: log.warning("Not supported declaration type \(node.kind) while calling importAccessor!") @@ -370,10 +399,10 @@ final class Swift2JavaVisitor { swiftDecl: node, name: name, apiKind: kind, - functionSignature: signature + functionSignature: signature, ) - if typeContext?.swiftNominal.isGeneric == true && imported.isStatic { + if typeContext?.swiftNominal.isGeneric == true && typeContext?.isSpecialization != true && imported.isStatic { log.debug("Skip importing static accessor in generic type: '\(node.qualifiedNameForDebug)'") return } @@ -390,7 +419,7 @@ final class Swift2JavaVisitor { private func synthesizeRawRepresentableConformance( enumDecl node: EnumDeclSyntax, - in parent: ImportedNominalType? + in parent: ImportedNominalType?, ) { guard let imported = translator.importedNominalType(node, parent: parent) else { return @@ -399,7 +428,7 @@ final class Swift2JavaVisitor { if let firstInheritanceType = imported.swiftNominal.firstInheritanceType, let inheritanceType = try? SwiftType( firstInheritanceType, - lookupContext: translator.lookupContext + lookupContext: translator.lookupContext, ), inheritanceType.isRawTypeCompatible { @@ -420,10 +449,175 @@ final class Swift2JavaVisitor { } } } + + // ==== ----------------------------------------------------------------------- + // MARK: Typealias declarations + + func visit( + typeAliasDecl node: TypeAliasDeclSyntax, + in typeContext: ImportedNominalType?, + sourceFilePath: String, + ) { + let javaName = node.name.text + let rhsType = node.initializer.value + + let genericArgs: [String] + if let identType = rhsType.as(IdentifierTypeSyntax.self) { + genericArgs = identType.genericArgumentClause?.arguments.compactMap { $0.argument.trimmedDescription } ?? [] + } else if let memberType = rhsType.as(MemberTypeSyntax.self) { + genericArgs = memberType.genericArgumentClause?.arguments.compactMap { $0.argument.trimmedDescription } ?? [] + } else { + return + } + + // Only register as specialization if the RHS has generic arguments + guard !genericArgs.isEmpty else { return } + + // Resolve the base type through the symbol table + guard let baseType = translator.importedNominalType(rhsType) else { + log.debug("Could not resolve base type for specialization: \(rhsType.trimmedDescription)") + return + } + + registerSpecialization( + javaName: javaName, + baseType: baseType, + genericArgs: genericArgs, + rhsDescription: rhsType.trimmedDescription, + ) + } + + /// Register a specialization from a typealias that specializes a generic type + private func registerSpecialization( + javaName: String, + baseType: ImportedNominalType, + genericArgs: [String], + rhsDescription: String, + ) { + // Build substitutions dict from the generic parameters + var substitutions: [String: String] = [:] + if baseType.swiftNominal.isGeneric { + let genericParams = baseType.swiftNominal.genericParameters.map { $0.name } + for (i, param) in genericParams.enumerated() { + if i < genericArgs.count { + substitutions[param] = genericArgs[i] + } + } + } + + let specialized: ImportedNominalType + do { + specialized = try baseType.specialize(as: javaName, with: substitutions) + } catch { + log.warning("Failed to specialize \(baseType.baseTypeName) as \(javaName): \(error)") + return + } + translator.specializations[baseType, default: []].insert(specialized) + log.info("Registered specialization: \(javaName) = \(rhsDescription)") + } + + // ==== ----------------------------------------------------------------------- + // MARK: Specialization support + + /// Apply specializations to a type if matching entries exist + func applySpecialization(to importedType: ImportedNominalType) { + guard let specializations = translator.specializations[importedType] else { + return + } + + for specialized in specializations { + translator.importedTypes[specialized.effectiveJavaName] = specialized + log.info("Applied specialization: \(specialized.effectiveJavaName) -> \(specialized.effectiveSwiftTypeName)") + } + } + + /// Apply specializations that were registered after their target types were visited, + /// then process any deferred constrained extensions + func applyPendingSpecializations() { + for (_, specializations) in translator.specializations { + for specialized in specializations { + if translator.importedTypes[specialized.effectiveJavaName] != nil { + continue + } + translator.importedTypes[specialized.effectiveJavaName] = specialized + log.info("Applied pending specialization: \(specialized.effectiveJavaName) -> \(specialized.effectiveSwiftTypeName)") + } + } + + // Process constrained extensions that were deferred + for (baseType, node, sourceFilePath) in deferredConstrainedExtensions { + let whereConstraints = parseWhereConstraints(node.genericWhereClause) + let matchingSpecializations = findMatchingSpecializations( + extendedType: baseType, + whereConstraints: whereConstraints, + ) + guard !matchingSpecializations.isEmpty else { + log.debug("Skipping deferred constrained extension of \(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) + } + } + } + deferredConstrainedExtensions.removeAll() + } + + // ==== ----------------------------------------------------------------------- + // 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] = [:] + for requirement in whereClause.requirements { + if case .sameTypeRequirement(let sameType) = requirement.requirement { + let lhs = sameType.leftType.trimmedDescription + let rhs = sameType.rightType.trimmedDescription + constraints[lhs] = rhs + } + } + return constraints + } + + /// Find specializations whose type args match the given where-clause constraints + private func findMatchingSpecializations( + extendedType: ImportedNominalType, + whereConstraints: [String: String], + ) -> [ImportedNominalType] { + guard let specializations = translator.specializations[extendedType] else { + return [] + } + return specializations.filter { specialized in + constraintsMatchSpecialization(whereConstraints, specialized: specialized) + } + } + + /// Check if where clause constraints match a specialization's generic arguments + private func constraintsMatchSpecialization( + _ constraints: [String: String], + specialized: ImportedNominalType, + ) -> Bool { + for (paramName, concreteType) in constraints { + if let expectedType = specialized.genericArguments[paramName] { + if expectedType != concreteType { + return false + } + } + // If the param isn't in the mapping, we allow it (might be a secondary constraint) + } + return true + } } extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyntax { func shouldExtract(config: Configuration, log: Logger, in parent: ImportedNominalType?) -> Bool { + // @JavaExport overrides all filters — always extract + if attributes.contains(where: { $0.isJavaExport }) { + return true + } + let meetsRequiredAccessLevel: Bool = switch config.effectiveMinimumInputAccessLevelMode { case .public: self.isPublic(in: parent?.swiftNominal.syntax) diff --git a/Sources/SwiftJava/Macros.swift b/Sources/SwiftJava/Macros.swift index 7c384425..756fb59f 100644 --- a/Sources/SwiftJava/Macros.swift +++ b/Sources/SwiftJava/Macros.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -192,3 +192,15 @@ public macro JavaStaticMethod(_ javaMethodName: String? = nil) = @attached(peer) public macro JavaImplementation(_ fullClassName: String) = #externalMacro(module: "SwiftJavaMacros", type: "JavaImplementationMacro") + +/// Marker macro that forces a Swift declaration to be exported to Java via jextract. +/// +/// When applied to a `typealias`, it registers a specialization entry for generic types: +/// ```swift +/// @JavaExport public typealias FishTank = Tank +/// ``` +/// This tells jextract to generate a concrete `FishTank` Java class for `Tank`, +/// even if `Tank` was included in the `filterExclude` configuration. +@attached(peer) +public macro JavaExport() = + #externalMacro(module: "SwiftJavaMacros", type: "JavaExportMacro") diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index 2f81934d..610094ef 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -112,21 +112,25 @@ public struct Configuration: Codable { importedModuleStubs?.keys.contains(moduleName) ?? false } - /// Monomorphization entries for generic types, mapping a qualified Swift type - /// name to a concrete specialization with a custom Java-facing name. + /// Specialization entries for generic types, mapping a Java-facing name + /// to its base Swift type and concrete type arguments. /// /// Example: /// ```json /// { - /// "monomorphize": { - /// "Tank": { - /// "javaName": "FishTank", + /// "specialize": { + /// "FishBox": { + /// "base": "Box", /// "typeArgs": {"Element": "Fish"} + /// }, + /// "PetBox": { + /// "base": "Box", + /// "typeArgs": {"Element": "Pet"} /// } /// } /// } /// ``` - public var monomorphize: [String: MonomorphizeEntry]? + public var specialize: [String: SpecializationConfigEntry]? // ==== wrap-java --------------------------------------------------------- @@ -468,19 +472,21 @@ public struct ConfigurationError: Error { } // ==== ----------------------------------------------------------------------- -// MARK: MonomorphizeEntry +// MARK: SpecializationConfigEntry -/// Configuration entry for monomorphizing a generic type into a concrete Java class -public struct MonomorphizeEntry: Codable, Sendable { - /// Mapping from generic parameter name to concrete type (e.g. {"T": "Fish"}) - public var typeArgs: [String: String] +/// Configuration entry for specializing a generic type into a concrete Java class. +/// The dictionary key is the Java-facing name; this entry provides the base type +/// and type argument mapping. +public struct SpecializationConfigEntry: Codable, Sendable { + /// The base Swift type name (e.g. "Box") + public var base: String - /// The Java-facing class name (e.g. "FishTank") - public var javaName: String + /// Mapping from generic parameter name to concrete type (e.g. {"Element": "Fish"}) + public var typeArgs: [String: String] - public init(typeArgs: [String: String], javaName: String) { + public init(base: String, typeArgs: [String: String]) { + self.base = base self.typeArgs = typeArgs - self.javaName = javaName } } diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index f1799af5..87bb5047 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -63,6 +63,7 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Dictionaries: `[String: Int]`, `[K:V]` | ❌ | ✅ | | Generic type: `struct S` | ❌ | ✅ | | Functions or properties using generic type param: `struct S { func f(_: T) {} }` | ❌ | ❌ | +| Generic type specialization and conditional extensions: `struct S{} extension S where T == Value {}` | ✅ | ❌ | | Static functions or properties in generic type | ❌ | ❌ | | Generic parameters in functions: `func f(x: T)` | ❌ | ✅ | | Generic return values in functions: `func f() -> T` | ❌ | ❌ | @@ -411,3 +412,53 @@ public final class MySwiftLibrary { public static MyID makeIntID(); } ``` + +### Specializing generic types + +> Note: Generic specialization is currently only supported in JNI mode. + +Because Swift's rich generics and extensions system, it is possible to encounter APIs which are not safely expressible in Java, +such as conditional/constrained extensions on types when an element is of specific type. + +A common example of this is e.g. a container type which gains additional methods when the element is of some type, like this: + +```swift +struct Box { + var name: String +} +``` + +which is extended with a conditional `where` clause: + +```swift +extension Box where Element == Fish { + func watchTheFish() { } +} +``` + +This method is not available on any `Box` and therefore we cannot safely expose it on the Java `Box` wrapper type. + +It would be possible to expose it and check at runtime if the `Box.Element` is of the expected type, this however +would result in runtime throws and is not an ideal experience when developers primarily use some specific _specialize_ +types like the `FishBox`: + +```swift +typealias FishBox = Box +``` + +The jextract tool will automatically detect typealiases like this and perform _specialization_ on them, i.e. a new +`FishBox` type will be exposed on the Java side, and it will have all matching extensions applied to it, i.e. it +will have the `watchTheFish()` method available in a type-safe and always known to work correctly way. + +In other words, this results in a Java class like this: + +```java +/// Specialization of `Fish`. +public final class FishBox ... { + + public void watchTheFish() { ... } +} +``` + +> NOTE: Currently no helpers are available to convert between unspecialized types to specialized ones, but this can be offered +> as additional `box.as(FishBox.class)` conversion methods in the future. \ No newline at end of file diff --git a/Sources/SwiftJavaMacros/JavaExportMacro.swift b/Sources/SwiftJavaMacros/JavaExportMacro.swift index c26b3526..90c1a18c 100644 --- a/Sources/SwiftJavaMacros/JavaExportMacro.swift +++ b/Sources/SwiftJavaMacros/JavaExportMacro.swift @@ -17,7 +17,7 @@ import SwiftSyntaxMacros /// Marker macro for jextract: forces a Swift declaration to be exported to Java. /// -/// When applied to a typealias, registers a monomorphization entry for generic types. +/// When applied to a typealias, registers a specialization entry for generic types. /// When applied to a nominal type, force-includes it for export regardless of filters. /// /// This macro produces no code — it is purely a marker read by the jextract tool. @@ -29,7 +29,6 @@ extension JavaExportMacro: PeerMacro { providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext, ) throws -> [DeclSyntax] { - // Marker-only macro — no code generation [] } } diff --git a/Sources/SwiftJavaMacros/SwiftJNIMacrosPlugin.swift b/Sources/SwiftJavaMacros/SwiftJNIMacrosPlugin.swift index 637b2ea4..cb76ea98 100644 --- a/Sources/SwiftJavaMacros/SwiftJNIMacrosPlugin.swift +++ b/Sources/SwiftJavaMacros/SwiftJNIMacrosPlugin.swift @@ -22,5 +22,6 @@ struct SwiftJavaMacrosPlugin: CompilerPlugin { JavaClassMacro.self, JavaFieldMacro.self, JavaMethodMacro.self, + JavaExportMacro.self, ] } diff --git a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMConfinedSwiftMemorySession.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMConfinedSwiftMemorySession.java index 05daebd7..645ea171 100644 --- a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMConfinedSwiftMemorySession.java +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMConfinedSwiftMemorySession.java @@ -19,7 +19,7 @@ import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; -final class FFMConfinedSwiftMemorySession extends ConfinedSwiftMemorySession implements AllocatingSwiftArena, ClosableAllocatingSwiftArena { +final class FFMConfinedSwiftMemorySession extends ConfinedSwiftMemorySession implements ClosableAllocatingSwiftArena { final Arena arena; public FFMConfinedSwiftMemorySession() { diff --git a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftAnyType.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftAnyType.java index 3915725b..e21ff95a 100644 --- a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftAnyType.java +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftAnyType.java @@ -14,9 +14,6 @@ package org.swift.swiftkit.ffm; -import org.swift.swiftkit.ffm.SwiftRuntime; -import org.swift.swiftkit.ffm.SwiftValueLayout; - import java.lang.foreign.GroupLayout; import java.lang.foreign.MemoryLayout; import java.lang.foreign.MemorySegment; @@ -30,10 +27,6 @@ public final class SwiftAnyType { private final MemorySegment memorySegment; public SwiftAnyType(MemorySegment memorySegment) { -// if (SwiftKit.getSwiftInt(memorySegment, 0) > 0) { -// throw new IllegalArgumentException("A Swift Any.Type cannot be null!"); -// } - this.memorySegment = memorySegment.asReadOnly(); } diff --git a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftRuntime.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftRuntime.java index 914efd81..94052336 100644 --- a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftRuntime.java +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftRuntime.java @@ -14,18 +14,15 @@ package org.swift.swiftkit.ffm; -import org.swift.swiftkit.core.SwiftInstance; import org.swift.swiftkit.core.CallTraces; import org.swift.swiftkit.core.SwiftLibraries; import org.swift.swiftkit.core.util.PlatformUtils; -import org.swift.swiftkit.ffm.SwiftRuntime.swiftjava; import java.lang.foreign.*; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; import java.util.*; -import java.util.stream.Collectors; import static org.swift.swiftkit.core.CallTraces.traceDowncall; import static org.swift.swiftkit.core.util.StringUtils.stripPrefix; diff --git a/Tests/JExtractSwiftTests/SpecializationTests.swift b/Tests/JExtractSwiftTests/SpecializationTests.swift new file mode 100644 index 00000000..e8670e36 --- /dev/null +++ b/Tests/JExtractSwiftTests/SpecializationTests.swift @@ -0,0 +1,326 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-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 SwiftJavaConfigurationShared +import Testing + +@testable import JExtractSwiftLib + +// ==== ----------------------------------------------------------------------- +// MARK: Specialization tests + +@Suite +struct SpecializationTests { + + // A generic type with two typealiases targeting the same base, + // and a constrained extension that only applies to one specialization + let multiSpecializationInput = + #""" + public struct Box { + public var items: [Element] + + public init() { + self.items = [] + } + + public func count() -> Int { + return items.count + } + } + + public struct Fish { + public var name: String + } + + public struct Pet { + public var name: String + } + + extension Box where Element == Fish { + public func observeTheFish() {} + } + + public typealias FishBox = Box + public typealias PetBox = Box + """# + + // ==== ----------------------------------------------------------------------- + // MARK: importedTypes structure + + @Test("Multiple specializations of same base type produce distinct importedTypes") + func multipleSpecializationsProduceDistinctTypes() throws { + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = Swift2JavaTranslator(config: config) + try translator.analyze(path: "/fake/Fake.swiftinterface", text: multiSpecializationInput) + + // Both specialized types should be registered + #expect(translator.importedTypes["FishBox"] != nil, "FishBox should be in importedTypes") + #expect(translator.importedTypes["PetBox"] != nil, "PetBox should be in importedTypes") + + // The base generic type remains in importedTypes (not removed) + let baseBox = try #require(translator.importedTypes["Box"]) + #expect(!baseBox.isSpecialization, "Base 'Box' should not be a specialization") + #expect(baseBox.genericParameterNames == ["Element"]) + #expect(baseBox.genericArguments.isEmpty) + #expect(!baseBox.isFullySpecialized) + + // Specialized types link back to their base + let fishBox = try #require(translator.importedTypes["FishBox"]) + let petBox = try #require(translator.importedTypes["PetBox"]) + #expect(fishBox.isSpecialization) + #expect(petBox.isSpecialization) + + // Verify effective names are distinct + #expect(fishBox.effectiveJavaName == "FishBox") + #expect(petBox.effectiveJavaName == "PetBox") + + #expect(fishBox.effectiveSwiftTypeName == "Box") + #expect(petBox.effectiveSwiftTypeName == "Box") + + // Verify new generic-model properties + #expect(fishBox.genericParameterNames == ["Element"]) + #expect(fishBox.genericArguments == ["Element": "Fish"]) + #expect(fishBox.isFullySpecialized) + #expect(fishBox.baseTypeName == "Box") + #expect(fishBox.specializedTypeName == "FishBox") + + #expect(petBox.genericParameterNames == ["Element"]) + #expect(petBox.genericArguments == ["Element": "Pet"]) + #expect(petBox.isFullySpecialized) + #expect(petBox.baseTypeName == "Box") + #expect(petBox.specializedTypeName == "PetBox") + + // Both wrappers delegate to the same base type + #expect(fishBox.specializationBaseType === petBox.specializationBaseType, "Both should wrap the same base Box type") + #expect(fishBox.specializationBaseType === translator.importedTypes["Box"], "Base should be the original Box") + } + + @Test("Specializations keyed by base type contain all entries") + func specializationEntriesContainAll() throws { + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = Swift2JavaTranslator(config: config) + try translator.analyze(path: "/fake/Fake.swiftinterface", text: multiSpecializationInput) + + let baseBox = try #require(translator.importedTypes["Box"]) + let specializations = try #require(translator.specializations[baseBox]) + #expect(specializations.count == 2, "Should have exactly 2 specializations for Box") + + let javaNames = specializations.map(\.effectiveJavaName).sorted() + #expect(javaNames == ["FishBox", "PetBox"]) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Generated Java classes + + @Test("FishBox Java class has base methods and constrained extension method") + func fishBoxJavaClass() throws { + try assertOutput( + input: multiSpecializationInput, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + // Class declaration + "public final class FishBox implements JNISwiftInstance {", + // Constructor must use the specialized name, not the base type name + "private FishBox(long selfPointer, SwiftArena swiftArena)", + // Factory method must use the specialized name + "public static FishBox wrapMemoryAddressUnsafe(long selfPointer, SwiftArena swiftArena)", + // Base method from Box + "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(", + ], + ) + } + + @Test("PetBox Java class has base methods but not Fish-constrained methods") + func petBoxJavaClass() throws { + try assertOutput( + input: multiSpecializationInput, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + // Class declaration + "public final class PetBox implements JNISwiftInstance {", + // Base method from Box + "public long count()", + ], + ) + + // Verify observeTheFish does NOT appear inside PetBox's class body + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = Swift2JavaTranslator(config: config) + try translator.analyze(path: "/fake/Fake.swiftinterface", text: multiSpecializationInput) + let petBox = try #require(translator.importedTypes["PetBox"]) + let methodNames = petBox.methods.map(\.name) + #expect(!methodNames.contains("observeTheFish"), "PetBox should not have Fish-constrained method") + } + + @Test("Single specialization generates expected Java class") + func singleSpecialization() throws { + let input = + #""" + public struct Box { + public var items: [Element] + + public init() { + self.items = [] + } + + public func count() -> Int { + return items.count + } + } + + public struct Fish { + public var name: String + } + + public typealias FishBox = Box + """# + + try assertOutput( + input: input, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + "public final class FishBox implements JNISwiftInstance {", + "public long count()", + ], + ) + } + + @Test("Nested generic specialization generates expected Java class") + func nestedGenericSpecialization() throws { + let input = + #""" + public struct Box { + public var items: [Element] + + public init() { + self.items = [] + } + + public func count() -> Int { + return items.count + } + } + + public struct Fish { + public var name: String + } + + public typealias FishBox = Box + public typealias FishBoxBox = Box> + """# + + try assertOutput( + input: input, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + "public final class FishBox implements JNISwiftInstance {", + "public final class FishBoxBox implements JNISwiftInstance {", + ], + ) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Swift thunks + + @Test("FishBox Swift thunks use direct downcall, not protocol opening") + func fishBoxSwiftThunks() throws { + try assertOutput( + input: multiSpecializationInput, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + // FishBox constrained extension method: direct downcall with concrete type + """ + @_cdecl("Java_com_example_swift_FishBox__00024observeTheFish__JJ") + public func Java_com_example_swift_FishBox__00024observeTheFish__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong, selfTypePointer: jlong) { + assert(selfPointer != 0, "selfPointer memory address was null") + let selfPointerBits$ = Int(Int64(fromJNI: selfPointer, in: environment)) + let selfPointer$ = UnsafeMutablePointer>(bitPattern: selfPointerBits$) + guard let selfPointer$ else { + fatalError("selfPointer memory address was null in call to \\(#function)!") + } + selfPointer$.pointee.observeTheFish() + } + """, + // FishBox base method: also uses direct downcall (not opening protocols) + """ + @_cdecl("Java_com_example_swift_FishBox__00024count__JJ") + public func Java_com_example_swift_FishBox__00024count__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong, selfTypePointer: jlong) -> jlong { + assert(selfPointer != 0, "selfPointer memory address was null") + let selfPointerBits$ = Int(Int64(fromJNI: selfPointer, in: environment)) + let selfPointer$ = UnsafeMutablePointer>(bitPattern: selfPointerBits$) + guard let selfPointer$ else { + fatalError("selfPointer memory address was null in call to \\(#function)!") + } + return Int64(selfPointer$.pointee.count()).getJNILocalRefValue(in: environment) + } + """, + ], + // FishBox must NOT use protocol opening — it's a concrete specialization + notExpectedChunks: [ + "_SwiftModule_FishBox_opener" + ], + ) + } + + // ==== ----------------------------------------------------------------------- + // MARK: Error cases + + @Test("Specializing a non-generic type throws an error") + func specializeNonGenericTypeThrows() throws { + var config = Configuration() + config.swiftModule = "SwiftModule" + let translator = Swift2JavaTranslator(config: config) + try translator.analyze( + path: "/fake/Fake.swiftinterface", + text: """ + public struct Fish { + public var name: String + } + """, + ) + + let fish = try #require(translator.importedTypes["Fish"]) + #expect(!fish.swiftNominal.isGeneric) + + #expect(throws: SpecializationError.self) { + _ = try fish.specialize(as: "FancyFish", with: ["T": "Int"]) + } + + do { + _ = try fish.specialize(as: "FancyFish", with: ["T": "Int"]) + } catch let error as SpecializationError { + #expect(error.message.contains("Unable to specialize non-generic type")) + #expect(error.message.contains("Fish")) + } + } +}