From b3bd8e195e24d2c8f391e70164fed38fc8146513 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Thu, 26 Sep 2024 13:46:17 -0700 Subject: [PATCH 1/5] Minor clean to remove unused field from ImportedClass --- Sources/JExtractSwift/ImportedDecls.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/JExtractSwift/ImportedDecls.swift b/Sources/JExtractSwift/ImportedDecls.swift index 725061f5..92b9f9d2 100644 --- a/Sources/JExtractSwift/ImportedDecls.swift +++ b/Sources/JExtractSwift/ImportedDecls.swift @@ -28,8 +28,6 @@ public struct ImportedProtocol: ImportedDecl { public struct ImportedClass: ImportedDecl { public var name: ImportedTypeName - public var implementedInterfaces: Set = [] - public var initializers: [ImportedFunc] = [] public var methods: [ImportedFunc] = [] @@ -126,6 +124,8 @@ public struct ImportedFunc: ImportedDecl, CustomStringConvertible { /// This is a full name such as init(cap:name:). public var identifier: String + /// This is the base identifier for the function, e.g., "init" for an + /// initializer or "f" for "f(a:b:)". public var baseIdentifier: String { guard let idx = identifier.firstIndex(of: "(") else { return identifier @@ -134,7 +134,7 @@ public struct ImportedFunc: ImportedDecl, CustomStringConvertible { } /// A display name to use to refer to the Swift declaration with its - /// enclosing type. + /// enclosing type, if there is one. public var displayName: String { if let parentName { return "\(parentName.swiftTypeName).\(identifier)" From 11e285da90cf50ddef5940c639052ea71b092aa3 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Thu, 26 Sep 2024 14:23:15 -0700 Subject: [PATCH 2/5] Maintain a Swift type name -> imported type mapping in jextract-swift Switch the representation of the imported types from a flat list over to a dictionary mapping from the Swift name to the imported type. We need this so that we can find information about the Swift type when it is referenced elsewhere in the syntax tree, such as extensions or uses of that type in other function signatures. While here, stop recording intermediate results within the visitor itself. Instead, keep a reference to the full translator and record results there incrementally. This will become more important when we start handling extensions. --- Sources/JExtractSwift/ImportedDecls.swift | 18 +++++- .../Swift2JavaTranslator+Printing.swift | 12 ++-- .../JExtractSwift/Swift2JavaTranslator.swift | 15 +++-- Sources/JExtractSwift/Swift2JavaVisitor.swift | 61 +++++++++---------- Sources/JExtractSwift/SwiftDylib.swift | 2 +- .../JExtractSwiftTests/FuncImportTests.swift | 16 ++--- 6 files changed, 62 insertions(+), 62 deletions(-) diff --git a/Sources/JExtractSwift/ImportedDecls.swift b/Sources/JExtractSwift/ImportedDecls.swift index 92b9f9d2..d6e0119e 100644 --- a/Sources/JExtractSwift/ImportedDecls.swift +++ b/Sources/JExtractSwift/ImportedDecls.swift @@ -25,17 +25,28 @@ public struct ImportedProtocol: ImportedDecl { public var identifier: String } -public struct ImportedClass: ImportedDecl { +/// Describes a Swift nominal type (e.g., a class, struct, enum) that has been +/// imported and is being translated into Java. +public struct ImportedNominalType: ImportedDecl { public var name: ImportedTypeName + public var kind: NominalTypeKind public var initializers: [ImportedFunc] = [] public var methods: [ImportedFunc] = [] - public init(name: ImportedTypeName) { + public init(name: ImportedTypeName, kind: NominalTypeKind) { self.name = name + self.kind = kind } } +public enum NominalTypeKind { + case `actor` + case `class` + case `enum` + case `struct` +} + public struct ImportedParam: Hashable { let param: FunctionParameterSyntax @@ -97,9 +108,10 @@ public struct ImportedTypeName: Hashable { javaType.className } - public init(swiftTypeName: String, javaType: JavaType) { + public init(swiftTypeName: String, javaType: JavaType, swiftMangledName: String? = nil) { self.swiftTypeName = swiftTypeName self.javaType = javaType + self.swiftMangledName = swiftMangledName ?? "" } } diff --git a/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift b/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift index ba55d12f..158b44ef 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift @@ -27,7 +27,7 @@ extension Swift2JavaTranslator { public func writeImportedTypesTo(outputDirectory: String) throws { var printer = CodePrinter() - for ty in importedTypes { + for (_, ty) in importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) { let filename = "\(ty.name.javaClassName!).java" log.info("Printing contents: \(filename)") printImportedClass(&printer, ty) @@ -106,7 +106,7 @@ extension Swift2JavaTranslator { } } - public func printImportedClass(_ printer: inout CodePrinter, _ decl: ImportedClass) { + public func printImportedClass(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { printHeader(&printer) printPackage(&printer) printImports(&printer) // TODO print any imports the file may need, it we talk to other Swift modules @@ -164,7 +164,7 @@ extension Swift2JavaTranslator { printer.print("") } - public func printClass(_ printer: inout CodePrinter, _ decl: ImportedClass, body: (inout CodePrinter) -> Void) { + public func printClass(_ printer: inout CodePrinter, _ decl: ImportedNominalType, body: (inout CodePrinter) -> Void) { printer.printTypeDecl("public final class \(decl.name.javaClassName!)") { printer in // ==== Storage of the class // FIXME: implement the self storage for the memory address and accessors @@ -270,7 +270,7 @@ extension Swift2JavaTranslator { ) } - private func printClassMemorySegmentConstructor(_ printer: inout CodePrinter, _ decl: ImportedClass) { + private func printClassMemorySegmentConstructor(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { printer.print( """ /** Instances are created using static {@code init} methods rather than through the constructor directly. */ @@ -282,7 +282,7 @@ extension Swift2JavaTranslator { } /// Print a property where we can store the "self" pointer of a class. - private func printClassSelfProperty(_ printer: inout CodePrinter, _ decl: ImportedClass) { + private func printClassSelfProperty(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { printer.print( """ // Pointer to the referred to class instance's "self". @@ -295,7 +295,7 @@ extension Swift2JavaTranslator { ) } - private func printClassMemoryLayout(_ printer: inout CodePrinter, _ decl: ImportedClass) { + private func printClassMemoryLayout(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { printer.print( """ private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( diff --git a/Sources/JExtractSwift/Swift2JavaTranslator.swift b/Sources/JExtractSwift/Swift2JavaTranslator.swift index e642d9ee..ec57d972 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator.swift @@ -37,7 +37,9 @@ public final class Swift2JavaTranslator { // TODO: consider how/if we need to store those etc public var importedGlobalFuncs: [ImportedFunc] = [] - public var importedTypes: [ImportedClass] = [] + /// A mapping from Swift type names (e.g., A.B) over to the imported nominal + /// type representation. + public var importedTypes: [String: ImportedNominalType] = [:] public init( javaPackage: String, @@ -83,13 +85,10 @@ extension Swift2JavaTranslator { let visitor = Swift2JavaVisitor( moduleName: self.swiftModuleName, targetJavaPackage: self.javaPackage, - log: log + translator: self ) visitor.walk(sourceFileSyntax) - self.importedGlobalFuncs.append(contentsOf: visitor.javaMethodDecls) - self.importedTypes.append(contentsOf: visitor.javaTypeDecls) - try await self.postProcessImportedDecls() } @@ -119,7 +118,7 @@ extension Swift2JavaTranslator { return funcDecl } - importedTypes = try await importedTypes._mapAsync { tyDecl in + importedTypes = Dictionary(uniqueKeysWithValues: try await importedTypes._mapAsync { (tyName, tyDecl) in var tyDecl = tyDecl log.info("Mapping type: \(tyDecl.name)") @@ -140,8 +139,8 @@ extension Swift2JavaTranslator { return funcDecl } - return tyDecl - } + return (tyName, tyDecl) + }) } } diff --git a/Sources/JExtractSwift/Swift2JavaVisitor.swift b/Sources/JExtractSwift/Swift2JavaVisitor.swift index 65337b8a..d05b83b8 100644 --- a/Sources/JExtractSwift/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwift/Swift2JavaVisitor.swift @@ -16,23 +16,23 @@ import SwiftParser import SwiftSyntax final class Swift2JavaVisitor: SyntaxVisitor { - let log: Logger + let translator: Swift2JavaTranslator /// The Swift module we're visiting declarations in let moduleName: String + /// The target java package we are going to generate types into eventually, /// store this along with type names as we import them. let targetJavaPackage: String - var javaMethodDecls: [ImportedFunc] = [] - var javaTypeDecls: [ImportedClass] = [] + var currentTypeName: String? = nil - var currentTypeDecl: ImportedClass? = nil + var log: Logger { translator.log } - init(moduleName: String, targetJavaPackage: String, log: Logger) { + init(moduleName: String, targetJavaPackage: String, translator: Swift2JavaTranslator) { self.moduleName = moduleName self.targetJavaPackage = targetJavaPackage - self.log = log + self.translator = translator super.init(viewMode: .all) } @@ -43,30 +43,28 @@ final class Swift2JavaVisitor: SyntaxVisitor { } log.info("Import: \(node.kind) \(node.name)") - currentTypeDecl = ImportedClass( + let typeName = node.name.text + currentTypeName = typeName + translator.importedTypes[typeName] = ImportedNominalType( // TODO: support nested classes (parent name here) name: ImportedTypeName( - swiftTypeName: node.name.text, + swiftTypeName: typeName, javaType: .class( package: targetJavaPackage, - name: node.name.text - ) - ) + name: typeName + ), + swiftMangledName: node.mangledNameFromComment + ), + kind: .class ) - // Retrieve the mangled name, if available. - if let mangledName = node.mangledNameFromComment { - currentTypeDecl!.name.swiftMangledName = mangledName - } - return .visitChildren } override func visitPost(_ node: ClassDeclSyntax) { - if let currentTypeDecl { + if currentTypeName != nil { log.info("Completed import: \(node.kind) \(node.name)") - self.javaTypeDecls.append(currentTypeDecl) - self.currentTypeDecl = nil + currentTypeName = nil } } @@ -113,7 +111,7 @@ final class Swift2JavaVisitor: SyntaxVisitor { let fullName = "\(node.name.text)(\(argumentLabelsStr))" var funcDecl = ImportedFunc( - parentName: currentTypeDecl?.name, + parentName: currentTypeName.map { translator.importedTypes[$0] }??.name, identifier: fullName, returnType: javaResultType, parameters: params @@ -125,26 +123,26 @@ final class Swift2JavaVisitor: SyntaxVisitor { funcDecl.swiftMangledName = mangledName } - if var currentTypeDecl = self.currentTypeDecl { - log.info("Record method in \(currentTypeDecl.name.javaType.description)") - currentTypeDecl.methods.append(funcDecl) - self.currentTypeDecl = currentTypeDecl + if let currentTypeName { + log.info("Record method in \(currentTypeName)") + translator.importedTypes[currentTypeName]?.methods.append(funcDecl) } else { - javaMethodDecls.append(funcDecl) + translator.importedGlobalFuncs.append(funcDecl) } return .skipChildren } override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { - guard var currentTypeDecl = self.currentTypeDecl else { + guard let currentTypeName, + let currentType = translator.importedTypes[currentTypeName] else { fatalError("Initializer must be within a current type, was: \(node)") } guard node.shouldImport(log: log) else { return .skipChildren } - self.log.info("Import initializer: \(node.kind) \(currentTypeDecl.name.javaType.description)") + self.log.info("Import initializer: \(node.kind) \(currentType.name.javaType.description)") let params: [ImportedParam] do { params = try node.signature.parameterClause.parameters.map { param in @@ -164,9 +162,9 @@ final class Swift2JavaVisitor: SyntaxVisitor { "init(\(params.compactMap { $0.effectiveName ?? "_" }.joined(separator: ":")))" var funcDecl = ImportedFunc( - parentName: currentTypeDecl.name, + parentName: currentType.name, identifier: initIdentifier, - returnType: currentTypeDecl.name, + returnType: currentType.name, parameters: params ) funcDecl.isInit = true @@ -177,9 +175,8 @@ final class Swift2JavaVisitor: SyntaxVisitor { funcDecl.swiftMangledName = mangledName } - log.info("Record initializer method in \(currentTypeDecl.name.javaType.description): \(funcDecl.identifier)") - currentTypeDecl.initializers.append(funcDecl) - self.currentTypeDecl = currentTypeDecl + log.info("Record initializer method in \(currentType.name.javaType.description): \(funcDecl.identifier)") + translator.importedTypes[currentTypeName]!.initializers.append(funcDecl) return .skipChildren } diff --git a/Sources/JExtractSwift/SwiftDylib.swift b/Sources/JExtractSwift/SwiftDylib.swift index 8be8f6a7..1f656ea7 100644 --- a/Sources/JExtractSwift/SwiftDylib.swift +++ b/Sources/JExtractSwift/SwiftDylib.swift @@ -31,7 +31,7 @@ package struct SwiftDylib { // FIXME: remove this entire utility; replace with self.log = Logger(label: "SwiftDylib(\(path))", logLevel: .trace) // TODO: take from env } - package func fillInTypeMangledName(_ decl: ImportedClass) async throws -> ImportedClass { + package func fillInTypeMangledName(_ decl: ImportedNominalType) async throws -> ImportedNominalType { // TODO: this is hacky, not precise at all and will be removed entirely guard decl.name.swiftMangledName.isEmpty else { // it was already processed diff --git a/Tests/JExtractSwiftTests/FuncImportTests.swift b/Tests/JExtractSwiftTests/FuncImportTests.swift index 57f7e0eb..57ff7173 100644 --- a/Tests/JExtractSwiftTests/FuncImportTests.swift +++ b/Tests/JExtractSwiftTests/FuncImportTests.swift @@ -181,9 +181,7 @@ final class MethodImportTests: XCTestCase { try await st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) - let funcDecl: ImportedFunc = st.importedTypes.first { - $0.name.javaClassName == "MySwiftClass" - }!.methods.first { + let funcDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.methods.first { $0.baseIdentifier == "helloMemberFunction" }! @@ -224,9 +222,7 @@ final class MethodImportTests: XCTestCase { try await st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) - let funcDecl: ImportedFunc = st.importedTypes.first { - $0.name.javaClassName == "MySwiftClass" - }!.methods.first { + let funcDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.methods.first { $0.baseIdentifier == "helloMemberFunction" }! @@ -267,9 +263,7 @@ final class MethodImportTests: XCTestCase { try await st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) - let funcDecl: ImportedFunc = st.importedTypes.first { - $0.name.javaClassName == "MySwiftClass" - }!.methods.first { + let funcDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.methods.first { $0.baseIdentifier == "helloMemberFunction" }! @@ -302,9 +296,7 @@ final class MethodImportTests: XCTestCase { try await st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) - let funcDecl: ImportedFunc = st.importedTypes.first { - $0.name.javaClassName == "MySwiftClass" - }!.methods.first { + let funcDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.methods.first { $0.baseIdentifier == "makeInt" }! From b9a29c5489b1bf878508bfeaadee523a94cd7be1 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Thu, 26 Sep 2024 21:45:27 -0700 Subject: [PATCH 3/5] Introduce basic support for resolving nominal types within single module Implement rudimentary name lookup and extension binding for nominal type names so we can resolve Swift type names within a module. --- .../JExtractSwift/NominalTypeResolution.swift | 352 ++++++++++++++++++ .../NominalTypeResolutionTests.swift | 63 ++++ 2 files changed, 415 insertions(+) create mode 100644 Sources/JExtractSwift/NominalTypeResolution.swift create mode 100644 Tests/JExtractSwiftTests/NominalTypeResolutionTests.swift diff --git a/Sources/JExtractSwift/NominalTypeResolution.swift b/Sources/JExtractSwift/NominalTypeResolution.swift new file mode 100644 index 00000000..e2649069 --- /dev/null +++ b/Sources/JExtractSwift/NominalTypeResolution.swift @@ -0,0 +1,352 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +/// Perform nominal type resolution, including the binding of extensions to +/// their extended nominal types and mapping type names to their full names. +@_spi(Testing) +public class NominalTypeResolution { + /// A syntax node for a nominal type declaration. + @_spi(Testing) + public typealias NominalTypeDeclSyntaxNode = any DeclGroupSyntax & NamedDeclSyntax + + /// Mapping from the syntax identifier for a given type declaration node, + /// such as StructDeclSyntax, to the set of extensions of this particular + /// type. + private var extensionsByType: [SyntaxIdentifier: [ExtensionDeclSyntax]] = [:] + + /// Mapping from extension declarations to the type declaration that they + /// extend. + private var resolvedExtensions: [ExtensionDeclSyntax: NominalTypeDeclSyntaxNode] = [:] + + /// Extensions that have been encountered but not yet resolved to + private var unresolvedExtensions: [ExtensionDeclSyntax] = [] + + /// Mapping from qualified nominal type names to their syntax nodes. + private var topLevelNominalTypes: [String: NominalTypeDeclSyntaxNode] = [:] + + @_spi(Testing) public init() { } +} + +// MARK: Nominal type name resolution. +extension NominalTypeResolution { + /// Compute the fully-qualified name of the given nominal type node. + /// + /// This produces the name that can be resolved back to the nominal type + /// via resolveNominalType(_:). + @_spi(Testing) + public func fullyQualifiedName(of node: NominalTypeDeclSyntaxNode) -> String? { + let nameComponents = fullyQualifiedNameComponents(of: node) + return nameComponents.isEmpty ? nil : nameComponents.joined(separator: ".") + } + + private func fullyQualifiedNameComponents(of node: NominalTypeDeclSyntaxNode) -> [String] { + var nameComponents: [String] = [] + + var currentNode = Syntax(node) + while true { + // If it's a nominal type, add its name. + if let nominal = currentNode.asProtocol(SyntaxProtocol.self) as? NominalTypeDeclSyntaxNode, + let nominalName = nominal.name.identifier?.name { + nameComponents.append(nominalName) + } + + // If it's an extension, add the full name of the extended type. + if let extensionDecl = currentNode.as(ExtensionDeclSyntax.self), + let extendedNominal = extendedType(of: extensionDecl) { + let extendedNominalNameComponents = fullyQualifiedNameComponents(of: extendedNominal) + return extendedNominalNameComponents + nameComponents.reversed() + } + + guard let parent = currentNode.parent else { + break + + } + currentNode = parent + } + + return nameComponents.reversed() + } + + /// Resolve a nominal type name to its syntax node, or nil if it cannot be + /// resolved for any reason. + @_spi(Testing) + public func resolveNominalType(_ name: String) -> NominalTypeDeclSyntaxNode? { + let components = name.split(separator: ".") + return resolveNominalType(components) + } + + /// Resolve a nominal type name to its syntax node, or nil if it cannot be + /// resolved for any reason. + private func resolveNominalType(_ nameComponents: some Sequence) -> NominalTypeDeclSyntaxNode? { + // Resolve the name components in order. + var currentNode: NominalTypeDeclSyntaxNode? = nil + for nameComponentStr in nameComponents { + let nameComponent = String(nameComponentStr) + + var nextNode: NominalTypeDeclSyntaxNode? = nil + if let currentNode { + nextNode = lookupNominalType(nameComponent, in: currentNode) + } else { + nextNode = topLevelNominalTypes[nameComponent] + } + + // If we couldn't resolve the next name, we're done. + guard let nextNode else { + return nil + } + + currentNode = nextNode + } + + return currentNode + } + + /// Look for a nominal type with the given name within this declaration group, + /// which could be a nominal type declaration or extension thereof. + private func lookupNominalType( + _ name: String, + inDeclGroup parentNode: some DeclGroupSyntax + ) -> NominalTypeDeclSyntaxNode? { + for member in parentNode.memberBlock.members { + let memberDecl = member.decl.asProtocol(DeclSyntaxProtocol.self) + + // If we have a member with the given name that is a nominal type + // declaration, we found what we're looking for. + if let namedMemberDecl = memberDecl.asProtocol(NamedDeclSyntax.self), + namedMemberDecl.name.identifier?.name == name, + let nominalTypeDecl = memberDecl as? NominalTypeDeclSyntaxNode + { + return nominalTypeDecl + } + } + + return nil + } + + /// Lookup nominal type name within a given nominal type. + private func lookupNominalType( + _ name: String, + in parentNode: NominalTypeDeclSyntaxNode + ) -> NominalTypeDeclSyntaxNode? { + // Look in the parent node itself. + if let found = lookupNominalType(name, inDeclGroup: parentNode) { + return found + } + + // Look in known extensions of the parent node. + if let extensions = extensionsByType[parentNode.id] { + for extensionDecl in extensions { + if let found = lookupNominalType(name, inDeclGroup: extensionDecl) { + return found + } + } + } + + return nil + } +} + +// MARK: Binding extensions +extension NominalTypeResolution { + /// Look up the nominal type declaration to which this extension is bound. + @_spi(Testing) + public func extendedType(of extensionDecl: ExtensionDeclSyntax) -> NominalTypeDeclSyntaxNode? { + return resolvedExtensions[extensionDecl] + } + + /// Bind all of the unresolved extensions to their nominal types. + /// + /// Returns the list of extensions that could not be resolved. + @_spi(Testing) + public func bindExtensions() -> [ExtensionDeclSyntax] { + while !unresolvedExtensions.isEmpty { + // Try to resolve all of the unresolved extensions. + let numExtensionsBefore = unresolvedExtensions.count + unresolvedExtensions.removeAll { extensionDecl in + // Try to resolve the type referenced by this extension declaration. If + // it fails, we'll try again later. + let nestedTypeNameComponents = extensionDecl.nestedTypeName + guard let resolvedType = resolveNominalType(nestedTypeNameComponents) else { + return false + } + + // We have successfully resolved the extended type. Record it and + // remove the extension from the list of unresolved extensions. + extensionsByType[resolvedType.id, default: []].append(extensionDecl) + resolvedExtensions[extensionDecl] = resolvedType + + return true + } + + // If we didn't resolve anything, we're done. + if numExtensionsBefore == unresolvedExtensions.count { + break + } + + assert(numExtensionsBefore > unresolvedExtensions.count) + } + + // Any unresolved extensions at this point are fundamentally unresolvable. + return unresolvedExtensions + } +} + +extension ExtensionDeclSyntax { + /// Produce the nested type name for the given + fileprivate var nestedTypeName: [String] { + var nameComponents: [String] = [] + var extendedType = extendedType + while true { + switch extendedType.as(TypeSyntaxEnum.self) { + case .attributedType(let attributedType): + extendedType = attributedType.baseType + continue + + case .identifierType(let identifierType): + guard let identifier = identifierType.name.identifier else { + return [] + } + + nameComponents.append(identifier.name) + return nameComponents.reversed() + + case .memberType(let memberType): + guard let identifier = memberType.name.identifier else { + return [] + } + + nameComponents.append(identifier.name) + extendedType = memberType.baseType + continue + + // Structural types implemented as nominal types. + case .arrayType: + return ["Array"] + + case .dictionaryType: + return ["Dictionary"] + + case .implicitlyUnwrappedOptionalType, .optionalType: + return [ "Optional" ] + + // Types that never involve nominals. + + case .classRestrictionType, .compositionType, .functionType, .metatypeType, + .missingType, .namedOpaqueReturnType, .packElementType, + .packExpansionType, .someOrAnyType, .suppressedType, .tupleType: + return [] + } + } + } +} + +// MARK: Adding source files to the resolution. +extension NominalTypeResolution { + /// Add the given source file. + @_spi(Testing) + public func addSourceFile(_ sourceFile: SourceFileSyntax) { + let visitor = NominalAndExtensionFinder(typeResolution: self) + visitor.walk(sourceFile) + } + + private class NominalAndExtensionFinder: SyntaxVisitor { + var typeResolution: NominalTypeResolution + var nestingDepth = 0 + + init(typeResolution: NominalTypeResolution) { + self.typeResolution = typeResolution + super.init(viewMode: .sourceAccurate) + } + + // Entering nominal type declarations. + + func visitNominal(_ node: NominalTypeDeclSyntaxNode) { + if nestingDepth == 0 { + typeResolution.topLevelNominalTypes[node.name.text] = node + } + + nestingDepth += 1 + } + + override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind { + visitNominal(node) + return .visitChildren + } + + override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { + visitNominal(node) + return .visitChildren + } + + override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { + visitNominal(node) + return .visitChildren + } + + override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { + visitNominal(node) + return .visitChildren + } + + override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { + visitNominal(node) + return .visitChildren + } + + // Exiting nominal type declarations. + func visitPostNominal(_ node: NominalTypeDeclSyntaxNode) { + assert(nestingDepth > 0) + nestingDepth -= 1 + } + + override func visitPost(_ node: ActorDeclSyntax) { + visitPostNominal(node) + } + + override func visitPost(_ node: ClassDeclSyntax) { + visitPostNominal(node) + } + + override func visitPost(_ node: EnumDeclSyntax) { + visitPostNominal(node) + } + + override func visitPost(_ node: ProtocolDeclSyntax) { + visitPostNominal(node) + } + + override func visitPost(_ node: StructDeclSyntax) { + visitPostNominal(node) + } + + // Extension handling + override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { + // Note that the extension is unresolved. We'll bind it later. + typeResolution.unresolvedExtensions.append(node) + nestingDepth += 1 + return .visitChildren + } + + override func visitPost(_ node: ExtensionDeclSyntax) { + nestingDepth -= 1 + } + + // Avoid stepping into functions. + + override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind { + return .skipChildren + } + } +} diff --git a/Tests/JExtractSwiftTests/NominalTypeResolutionTests.swift b/Tests/JExtractSwiftTests/NominalTypeResolutionTests.swift new file mode 100644 index 00000000..88b181b1 --- /dev/null +++ b/Tests/JExtractSwiftTests/NominalTypeResolutionTests.swift @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@_spi(Testing) import JExtractSwift +import SwiftSyntax +import SwiftParser +import Testing + +@Suite("Nominal type lookup") +struct NominalTypeLookupSuite { + func checkNominalRoundTrip( + _ resolution: NominalTypeResolution, + name: String, + fileID: String = #fileID, + fileParh: String = #filePath, + line: Int = #line, + column: Int = #column + ) { + let sourceLocation = SourceLocation(fileID: fileID, filePath: fileParh, line: line, column: column) + let nominal = resolution.resolveNominalType(name) + #expect(nominal != nil, sourceLocation: sourceLocation) + if let nominal { + #expect(resolution.fullyQualifiedName(of: nominal) == name, sourceLocation: sourceLocation) + } + } + + @Test func lookupBindingTests() { + let resolution = NominalTypeResolution() + resolution.addSourceFile(""" + extension X { + struct Y { + } + } + + struct X { + } + + extension X.Y { + struct Z { } + } + """) + + // Bind all extensions and verify that all were bound. + #expect(resolution.bindExtensions().isEmpty) + + checkNominalRoundTrip(resolution, name: "X") + checkNominalRoundTrip(resolution, name: "X.Y") + checkNominalRoundTrip(resolution, name: "X.Y.Z") + #expect(resolution.resolveNominalType("Y") == nil) + #expect(resolution.resolveNominalType("X.Z") == nil) + } +} + From 42a64e49f03dc6d7d83a656c2665d69b20ea897d Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Thu, 26 Sep 2024 22:49:42 -0700 Subject: [PATCH 4/5] Use nominal type resolution to translate members of extensions Use the new nominal type resolution functionality within jextract-swift to resolve extensions to their corresponding nominal types, so that members of those extensions show up as member of the resulting Java class. --- .../JExtractSwift/NominalTypeResolution.swift | 9 +-- .../JExtractSwift/Swift2JavaTranslator.swift | 6 ++ Sources/JExtractSwift/Swift2JavaVisitor.swift | 64 +++++++++++++++---- .../JExtractSwiftTests/FuncImportTests.swift | 46 +++++++++++++ 4 files changed, 108 insertions(+), 17 deletions(-) diff --git a/Sources/JExtractSwift/NominalTypeResolution.swift b/Sources/JExtractSwift/NominalTypeResolution.swift index e2649069..4cb4d34a 100644 --- a/Sources/JExtractSwift/NominalTypeResolution.swift +++ b/Sources/JExtractSwift/NominalTypeResolution.swift @@ -17,10 +17,6 @@ import SwiftSyntax /// their extended nominal types and mapping type names to their full names. @_spi(Testing) public class NominalTypeResolution { - /// A syntax node for a nominal type declaration. - @_spi(Testing) - public typealias NominalTypeDeclSyntaxNode = any DeclGroupSyntax & NamedDeclSyntax - /// Mapping from the syntax identifier for a given type declaration node, /// such as StructDeclSyntax, to the set of extensions of this particular /// type. @@ -39,6 +35,10 @@ public class NominalTypeResolution { @_spi(Testing) public init() { } } +/// A syntax node for a nominal type declaration. +@_spi(Testing) +public typealias NominalTypeDeclSyntaxNode = any DeclGroupSyntax & NamedDeclSyntax + // MARK: Nominal type name resolution. extension NominalTypeResolution { /// Compute the fully-qualified name of the given nominal type node. @@ -170,6 +170,7 @@ extension NominalTypeResolution { /// /// Returns the list of extensions that could not be resolved. @_spi(Testing) + @discardableResult public func bindExtensions() -> [ExtensionDeclSyntax] { while !unresolvedExtensions.isEmpty { // Try to resolve all of the unresolved extensions. diff --git a/Sources/JExtractSwift/Swift2JavaTranslator.swift b/Sources/JExtractSwift/Swift2JavaTranslator.swift index ec57d972..d5396872 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator.swift @@ -41,6 +41,8 @@ public final class Swift2JavaTranslator { /// type representation. public var importedTypes: [String: ImportedNominalType] = [:] + let nominalResolution: NominalTypeResolution = NominalTypeResolution() + public init( javaPackage: String, swiftModuleName: String @@ -82,6 +84,10 @@ extension Swift2JavaTranslator { let sourceFileSyntax = Parser.parse(source: text) + // Find all of the types and extensions, then bind the extensions. + nominalResolution.addSourceFile(sourceFileSyntax) + nominalResolution.bindExtensions() + let visitor = Swift2JavaVisitor( moduleName: self.swiftModuleName, targetJavaPackage: self.javaPackage, diff --git a/Sources/JExtractSwift/Swift2JavaVisitor.swift b/Sources/JExtractSwift/Swift2JavaVisitor.swift index d05b83b8..db4625bb 100644 --- a/Sources/JExtractSwift/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwift/Swift2JavaVisitor.swift @@ -25,6 +25,7 @@ final class Swift2JavaVisitor: SyntaxVisitor { /// store this along with type names as we import them. let targetJavaPackage: String + /// The current type name as a nested name like A.B.C. var currentTypeName: String? = nil var log: Logger { translator.log } @@ -37,27 +38,46 @@ final class Swift2JavaVisitor: SyntaxVisitor { super.init(viewMode: .all) } - override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { - guard node.shouldImport(log: log) else { - return .skipChildren + /// Try to resolve the given nominal type node into its imported + /// representation. + func resolveNominalType( + _ nominal: some DeclGroupSyntax & NamedDeclSyntax, + kind: NominalTypeKind + ) -> ImportedNominalType? { + if !nominal.shouldImport(log: log) { + return nil + } + + guard let fullName = translator.nominalResolution.fullyQualifiedName(of: nominal) else { + return nil + } + + if let alreadyImported = translator.importedTypes[fullName] { + return alreadyImported } - log.info("Import: \(node.kind) \(node.name)") - let typeName = node.name.text - currentTypeName = typeName - translator.importedTypes[typeName] = ImportedNominalType( - // TODO: support nested classes (parent name here) + let importedNominal = ImportedNominalType( name: ImportedTypeName( - swiftTypeName: typeName, + swiftTypeName: fullName, javaType: .class( package: targetJavaPackage, - name: typeName + name: fullName ), - swiftMangledName: node.mangledNameFromComment + swiftMangledName: nominal.mangledNameFromComment ), - kind: .class + kind: kind ) + translator.importedTypes[fullName] = importedNominal + return importedNominal + } + + override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { + guard let importedNominalType = resolveNominalType(node, kind: .class) else { + return .skipChildren + } + + currentTypeName = importedNominalType.name.swiftTypeName return .visitChildren } @@ -68,6 +88,24 @@ final class Swift2JavaVisitor: SyntaxVisitor { } } + override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { + // Resolve the extended type of the extension as an imported nominal, and + // recurse if we found it. + guard let nominal = translator.nominalResolution.extendedType(of: node), + let importedNominalType = resolveNominalType(nominal, kind: .class) else { + return .skipChildren + } + + currentTypeName = importedNominalType.name.swiftTypeName + return .visitChildren + } + + override func visitPost(_ node: ExtensionDeclSyntax) { + if currentTypeName != nil { + currentTypeName = nil + } + } + override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { guard node.shouldImport(log: log) else { return .skipChildren @@ -182,7 +220,7 @@ final class Swift2JavaVisitor: SyntaxVisitor { } } -extension ClassDeclSyntax { +extension DeclGroupSyntax where Self: NamedDeclSyntax { func shouldImport(log: Logger) -> Bool { guard (accessControlModifiers.first { $0.isPublic }) != nil else { log.trace("Cannot import \(self.name) because: is not public") diff --git a/Tests/JExtractSwiftTests/FuncImportTests.swift b/Tests/JExtractSwiftTests/FuncImportTests.swift index 57ff7173..e670cd77 100644 --- a/Tests/JExtractSwiftTests/FuncImportTests.swift +++ b/Tests/JExtractSwiftTests/FuncImportTests.swift @@ -37,6 +37,11 @@ final class MethodImportTests: XCTestCase { // MANGLED NAME: $s14MySwiftLibrary23globalTakeLongIntString1l3i321sys5Int64V_s5Int32VSStF public func globalTakeIntLongString(i32: Int32, l: Int64, s: String) + extension MySwiftClass { + // MANGLED NAME: $s14MySwiftLibrary0aB5ClassC22helloMemberFunctionInExtension + public func helloMemberInExtension() + } + // MANGLED NAME: $s14MySwiftLibrary0aB5ClassCMa public class MySwiftClass { // MANGLED NAME: $s14MySwiftLibrary0aB5ClassC3len3capACSi_SitcfC @@ -213,6 +218,47 @@ final class MethodImportTests: XCTestCase { ) } + func test_method_class_helloMemberInExtension_self_memorySegment() async throws { + let st = Swift2JavaTranslator( + javaPackage: "com.example.swift", + swiftModuleName: "__FakeModule" + ) + st.log.logLevel = .trace + + try await st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) + + let funcDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.methods.first { + $0.baseIdentifier == "helloMemberInExtension" + }! + + let output = CodePrinter.toString { printer in + st.printFuncDowncallMethod(&printer, decl: funcDecl, selfVariant: .memorySegment) + } + + assertOutput( + output, + expected: + """ + /** + * {@snippet lang=swift : + * public func helloMemberInExtension() + * } + */ + public static void helloMemberInExtension(java.lang.foreign.MemorySegment self$) { + var mh$ = helloMemberInExtension.HANDLE; + try { + if (TRACE_DOWNCALLS) { + traceDowncall(self$); + } + mh$.invokeExact(self$); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + """ + ) + } + func test_method_class_helloMemberFunction_self_wrapper() async throws { let st = Swift2JavaTranslator( javaPackage: "com.example.swift", From f32ea3194d12afd1b6b167d1e918c774deabbfd9 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Fri, 27 Sep 2024 10:48:19 +0100 Subject: [PATCH 5/5] Update Sources/JExtractSwift/NominalTypeResolution.swift --- Sources/JExtractSwift/NominalTypeResolution.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JExtractSwift/NominalTypeResolution.swift b/Sources/JExtractSwift/NominalTypeResolution.swift index 4cb4d34a..c655598c 100644 --- a/Sources/JExtractSwift/NominalTypeResolution.swift +++ b/Sources/JExtractSwift/NominalTypeResolution.swift @@ -205,7 +205,7 @@ extension NominalTypeResolution { } extension ExtensionDeclSyntax { - /// Produce the nested type name for the given + /// Produce the nested type name for the given decl fileprivate var nestedTypeName: [String] { var nameComponents: [String] = [] var extendedType = extendedType